1315 lines · 44.8 KB · TypeScript
RawDownload
import type {  EnvironmentId,  ModelSelection,  ProviderInstanceId,  ProviderOptionDescriptor,  ProviderOptionSelection,  RuntimeMode,} from "@t3tools/contracts";import type { LegendListRenderItemProps } from "@legendapp/list/react-native";import { AnimatedLegendList } from "@legendapp/list/reanimated";import { HeaderHeightContext } from "@react-navigation/elements";import {  getProviderOptionCurrentLabel,  getProviderOptionCurrentValue,  getProviderOptionDescriptors,} from "@t3tools/shared/model";import { useNavigation, useRoute, type RouteProp } from "@react-navigation/native";import {  createNativeStackNavigator,  type NativeStackNavigationProp,} from "@react-navigation/native-stack";import * as Haptics from "expo-haptics";import {  createContext,  use,  useCallback,  useEffect,  useMemo,  useState,  type ReactNode,} from "react";import { Alert, Platform, Pressable, ScrollView, TextInput, View } from "react-native";import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SymbolView } from "../../components/AppSymbol";import { AppText as Text } from "../../components/AppText";import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";import { ProviderIcon } from "../../components/ProviderIcon";import { ThemedSwitch } from "../../components/ThemedSwitch";import { cn } from "../../lib/cn";import type { ModelOption, ProviderGroup } from "../../lib/modelOptions";import { applyProviderOptionSelection } from "../../lib/providerOptions";import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";import { useUniwindTheme } from "../../lib/useUniwindTheme";import {  NativeHeaderToolbar,  NativeStackScreenOptions,  nativeHeaderScrollEdgeEffects,} from "../../native/StackHeader";import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";import { serverEnvironment } from "../../state/server";import { useAtomCommand } from "../../state/use-atom-command";import { useNewTaskFlow } from "./new-task-flow-provider";import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";import {  createProviderCatalogRefreshRunner,  providerCatalogRefreshError,} from "./provider-catalog-refresh";import {  createNativeMailSearchToolbarItem,  NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET,  NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,} from "../layout/native-mail-search-toolbar";import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options";import {  canCommitPendingModel,  modelMatchesCatalogQuery,  pendingModelAfterPress,  providerSectionIsCollapsed,} from "./thread-settings-sheet-state"; /** * Everyday harnesses start expanded; every other provider (OpenRouter catalogs * and friends) starts folded so a 300-model catalog cannot bury the list. All * provider headers remain user-collapsible. */const PRIMARY_PROVIDER_DRIVERS: ReadonlySet<string> = new Set([  "claudeAgent",  "codex",  "antigravity",]);/** * Keep measured row changes stable, but let catalog mutations use the list's * native bounds so a filtered catalog that underflows returns to the top. */const THREAD_SETTINGS_MAINTAIN_VISIBLE_CONTENT_POSITION = {  data: false,  size: true,} as const;const THREAD_SETTINGS_CATALOG_LAYOUT_TRANSITION = LinearTransition.duration(180);const THREAD_SETTINGS_CATALOG_ENTER_TRANSITION = FadeIn.duration(140);const THREAD_SETTINGS_CATALOG_EXIT_TRANSITION = FadeOut.duration(120);const THREAD_SETTINGS_OPTIONS_LAYOUT_TRANSITION = LinearTransition.duration(180);const THREAD_SETTINGS_OPTION_ENTER_TRANSITION = FadeIn.duration(140);const THREAD_SETTINGS_OPTION_EXIT_TRANSITION = FadeOut.duration(100);const THREAD_SETTINGS_HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(  Platform.OS,  Platform.Version,);function ModelRow(props: {  readonly option: ModelOption;  readonly selected: boolean;  readonly onPress: () => void;  readonly isFirst: boolean;  readonly isLast: boolean;}) {  const { materialYouStyleLayoutActive } = useAppearancePreferences();  const selectedMaterialRow = materialYouStyleLayoutActive && props.selected;  return (    <Pressable      accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}      accessibilityRole="radio"      accessibilityState={{        checked: props.selected,        disabled: props.option.isUnavailable === true,      }}      disabled={props.option.isUnavailable}      onPress={props.onPress}      className={cn(        "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4 py-2 active:bg-subtle",        selectedMaterialRow && "bg-thread-selected",        props.isFirst && "rounded-t-2xl",        props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",      )}    >      <View className="min-w-0 flex-1">        <View className="flex-row items-center gap-2">          <Text            className="min-w-0 shrink text-base font-t3-medium text-foreground"            numberOfLines={1}          >            {props.option.label}          </Text>          {props.option.isDefault ? (            <View className="rounded-md bg-subtle-strong px-1.5 py-0.5">              <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text>            </View>          ) : null}          {props.option.isLegacy ? (            <View className="rounded-md bg-subtle px-1.5 py-0.5">              <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text>            </View>          ) : null}          {props.option.isUnavailable ? (            <Text className="text-xs text-foreground">Unavailable</Text>          ) : null}        </View>        {props.option.subtitle ? (          <Text className="text-xs text-foreground-muted" numberOfLines={1}>            {props.option.subtitle}          </Text>        ) : null}      </View>      {props.selected ? (        <SymbolView          name="checkmark"          size={16}          tintColorClassName={"accent-icon"}          type="monochrome"          weight="semibold"        />      ) : null}    </Pressable>  );} /** Provider catalog header with its harness logo and disclosure state. */function ProviderHeader(props: {  readonly driver: string | undefined;  readonly label: string;  readonly collapsible: boolean;  readonly collapsed: boolean;  readonly modelCount: number;  readonly onToggle: () => void;}) {  const content = (    <>      <ProviderIcon provider={props.driver} size={15} />      <Text className="text-sm font-t3-medium text-foreground-muted">{props.label}</Text>      {props.collapsible ? (        <>          <View className="flex-1" />          {props.collapsed ? (            <Text className="text-2xs font-t3-medium text-foreground-muted">              {props.modelCount}            </Text>          ) : null}          <SymbolView            name={props.collapsed ? "chevron.down" : "chevron.up"}            size={12}            tintColorClassName={"accent-icon-subtle"}            type="monochrome"          />        </>      ) : null}    </>  );   if (props.collapsible) {    return (      <Pressable        accessibilityLabel={`${props.label}, ${props.modelCount} models`}        accessibilityRole="button"        accessibilityState={{ expanded: !props.collapsed }}        className="mx-4 mt-1 min-h-11 flex-row items-center gap-2 rounded-xl px-1 pt-2 active:opacity-60"        onPress={props.onToggle}      >        {content}      </Pressable>    );  }   return (    <View accessibilityRole="header" className="mx-4 min-h-9 flex-row items-center gap-2 px-1 pt-1">      {content}    </View>  );} /** Compact row that opens a single-choice submenu panel. */function DisclosureRow(props: {  readonly label: string;  readonly value: string | undefined;  readonly onPress: () => void;  readonly isLast?: boolean;}) {  return (    <Pressable      accessibilityRole="button"      onPress={props.onPress}      className={cn(        "min-h-11 flex-row items-center gap-2 bg-card px-4 py-2 active:bg-subtle",        !props.isLast && "border-b border-border-subtle",      )}    >      <Text className="text-sm font-t3-medium text-foreground">{props.label}</Text>      <View className="flex-1" />      {props.value ? (        <Text className="text-sm text-foreground-muted" numberOfLines={1}>          {props.value}        </Text>      ) : null}      <SymbolView        name="chevron.right"        size={12}        tintColorClassName={"accent-icon-subtle"}        type="monochrome"      />    </Pressable>  );} /** Single option inside a submenu panel. */function ChoiceRow(props: {  readonly label: string;  readonly description?: string;  readonly selected: boolean;  readonly onPress: () => void;  readonly isLast: boolean;}) {  return (    <Pressable      accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}      accessibilityRole="radio"      accessibilityState={{ checked: props.selected }}      onPress={props.onPress}      className={cn(        "min-h-14 flex-row items-center gap-3 bg-card px-4 py-3 active:bg-subtle",        !props.isLast && "border-b border-border-subtle",      )}    >      <View className="min-w-0 flex-1 gap-0.5">        <Text className="text-base font-t3-medium text-foreground">{props.label}</Text>        {props.description ? (          <Text className="text-sm leading-5 text-foreground-muted">{props.description}</Text>        ) : null}      </View>      {props.selected ? (        <SymbolView          name="checkmark"          size={16}          tintColorClassName={"accent-icon"}          type="monochrome"          weight="semibold"        />      ) : null}    </Pressable>  );} function SwitchRow(props: {  readonly label: string;  readonly value: boolean;  readonly onValueChange: (value: boolean) => void;  readonly isLast?: boolean;}) {  return (    <View      className={cn(        "min-h-11 flex-row items-center justify-between bg-card px-4 py-1",        !props.isLast && "border-b border-border-subtle",      )}    >      <Text className="text-sm font-t3-medium text-foreground">{props.label}</Text>      <ThemedSwitch        accessibilityLabel={props.label}        onValueChange={props.onValueChange}        value={props.value}      />    </View>  );} type ThreadSettingsSubmenuPage =  | { readonly kind: "descriptor"; readonly id: string }  | { readonly kind: "runtime" }; type ThreadSettingsSessionProps = {  readonly environmentId: EnvironmentId | null;  readonly providerInstanceId?: ProviderInstanceId;  readonly providerGroups: ReadonlyArray<ProviderGroup>;  readonly selectedModel: ModelSelection | null;  readonly onSelectModel: (option: ModelOption) => void;  readonly optionDescriptors: ReadonlyArray<ProviderOptionDescriptor>;  readonly onUpdateOptionSelections: (selections: ReadonlyArray<ProviderOptionSelection>) => void;  readonly runtimeMode: RuntimeMode;  readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;}; export type ExistingThreadSettingsRouteSession = ThreadSettingsSessionProps & {  readonly ownerId: string;}; type ExistingThreadSettingsRouteContextValue = {  readonly session: ExistingThreadSettingsRouteSession | null;  readonly present: (session: ExistingThreadSettingsRouteSession) => void;  readonly clear: (ownerId: string) => void;}; const ExistingThreadSettingsRouteContext =  createContext<ExistingThreadSettingsRouteContextValue | null>(null); /** Bridges the active thread's settings state into the root native sheet route. */export function ExistingThreadSettingsRouteProvider(props: { readonly children: ReactNode }) {  const [session, setSession] = useState<ExistingThreadSettingsRouteSession | null>(null);  const present = useCallback((nextSession: ExistingThreadSettingsRouteSession) => {    setSession(nextSession);  }, []);  const clear = useCallback((ownerId: string) => {    setSession((current) => (current?.ownerId === ownerId ? null : current));  }, []);  const value = useMemo(() => ({ session, present, clear }), [clear, present, session]);   return (    <ExistingThreadSettingsRouteContext.Provider value={value}>      {props.children}    </ExistingThreadSettingsRouteContext.Provider>  );} export function useExistingThreadSettingsRoutePresentation() {  const value = use(ExistingThreadSettingsRouteContext);  if (!value) {    throw new Error(      "useExistingThreadSettingsRoutePresentation must be used inside ExistingThreadSettingsRouteProvider.",    );  }  return value;} type ThreadSettingsSessionValue = {  readonly environmentId: EnvironmentId | null;  readonly providerInstanceId?: ProviderInstanceId;  readonly providerGroups: ReadonlyArray<ProviderGroup>;  readonly runtimeMode: RuntimeMode;  readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void;  readonly displayedDescriptors: ReadonlyArray<ProviderOptionDescriptor>;  readonly providerExpansionOverrides: ReadonlySet<string>;  readonly hasLegacyModels: boolean;  readonly pendingModel: ModelOption | null;  readonly providerFilter: string | null;  readonly searchQuery: string;  readonly showLegacy: boolean;  readonly applyOptionChange: (id: string, value: string | boolean) => void;  readonly commitPendingModel: () => boolean;  readonly isApplied: (option: ModelOption) => boolean;  readonly isDisplayed: (option: ModelOption) => boolean;  readonly pressModel: (option: ModelOption) => void;  readonly setProviderFilter: (providerKey: string | null) => void;  readonly setSearchQuery: (query: string) => void;  readonly setShowLegacy: (showLegacy: boolean) => void;  readonly toggleProvider: (providerKey: string) => void;}; const ThreadSettingsSessionContext = createContext<ThreadSettingsSessionValue | null>(null); /** Owns the staged model and option state for one picker presentation. */function ThreadSettingsSessionProvider(  props: ThreadSettingsSessionProps & { readonly children: ReactNode },) {  const [showLegacyToggle, setShowLegacyToggle] = useState(false);  const [providerFilter, setProviderFilter] = useState<string | null>(null);  const [searchQuery, setSearchQuery] = useState("");  const [providerExpansionOverrides, setProviderExpansionOverrides] = useState<ReadonlySet<string>>(    () => new Set(),  );  const [pendingModel, setPendingModel] = useState<ModelOption | null>(null);   const isApplied = useCallback(    (option: ModelOption) =>      option.selection.instanceId === props.selectedModel?.instanceId &&      option.selection.model === props.selectedModel.model,    [props.selectedModel],  );  // The list highlights the staged pick; Save turns it into the applied one.  const isDisplayed = useCallback(    (option: ModelOption) => (pendingModel ? option.key === pendingModel.key : isApplied(option)),    [isApplied, pendingModel],  );   // While a model is staged, the settings rows describe and edit the staged  // model's options (kept on its pending selection); Save applies model and  // options together. Otherwise they edit the applied selection directly.  const displayedDescriptors = useMemo(    () =>      pendingModel        ? pendingModel.capabilities          ? getProviderOptionDescriptors({              caps: pendingModel.capabilities,              selections: pendingModel.selection.options,            })          : []        : props.optionDescriptors,    [pendingModel, props.optionDescriptors],  );   const hasLegacyModels = useMemo(    () => props.providerGroups.some((group) => group.models.some((model) => model.isLegacy)),    [props.providerGroups],  );  const commitPendingModel = useCallback(() => {    if (pendingModel) {      if (!canCommitPendingModel(pendingModel, props.providerGroups)) {        Alert.alert(          "Model unavailable",          "Set up this provider on web or desktop, or select another model.",        );        return false;      }      void Haptics.selectionAsync();      props.onSelectModel(pendingModel);    }    return true;  }, [pendingModel, props.onSelectModel, props.providerGroups]);   const applyOptionChange = useCallback(    (id: string, value: string | boolean) => {      const next = applyProviderOptionSelection(displayedDescriptors, { id, value });      if (!next) {        return;      }      if (pendingModel) {        setPendingModel({          ...pendingModel,          selection: { ...pendingModel.selection, options: next },        });      } else {        props.onUpdateOptionSelections(next);      }    },    [displayedDescriptors, pendingModel, props.onUpdateOptionSelections],  );   const toggleProvider = useCallback((providerKey: string) => {    setProviderExpansionOverrides((current) => {      const next = new Set(current);      if (!next.delete(providerKey)) {        next.add(providerKey);      }      return next;    });  }, []);   const pressModel = useCallback(    (option: ModelOption) => {      void Haptics.selectionAsync();      setPendingModel((current) =>        pendingModelAfterPress({          current,          pressed: option,          pressedIsApplied: isApplied(option),        }),      );    },    [isApplied],  );   const value = useMemo<ThreadSettingsSessionValue>(    () => ({

Lines 1–500 of 1315.