259 lines · 6.9 KB · TypeScript
RawDownload
import { useCallback, useSyncExternalStore } from "react";import { Option, Schema } from "effect";import { type ProviderKind } from "@t3tools/contracts";import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";const MAX_CUSTOM_MODEL_COUNT = 32;export const MAX_CUSTOM_MODEL_LENGTH = 256;const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {  codex: new Set(getModelOptions("codex").map((option) => option.slug)),}; const AppSettingsSchema = Schema.Struct({  codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(    Schema.withConstructorDefault(() => Option.some("")),  ),  codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(    Schema.withConstructorDefault(() => Option.some("")),  ),  confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),  enableAssistantStreaming: Schema.Boolean.pipe(    Schema.withConstructorDefault(() => Option.some(false)),  ),  customCodexModels: Schema.Array(Schema.String).pipe(    Schema.withConstructorDefault(() => Option.some([])),  ),});export type AppSettings = typeof AppSettingsSchema.Type;export interface AppModelOption {  slug: string;  name: string;  isCustom: boolean;} const DEFAULT_APP_SETTINGS = AppSettingsSchema.makeUnsafe({}); let listeners: Array<() => void> = [];let cachedRawSettings: string | null | undefined;let cachedSnapshot: AppSettings = DEFAULT_APP_SETTINGS; export function normalizeCustomModelSlugs(  models: Iterable<string | null | undefined>,  provider: ProviderKind = "codex",): string[] {  const normalizedModels: string[] = [];  const seen = new Set<string>();  const builtInModelSlugs = BUILT_IN_MODEL_SLUGS_BY_PROVIDER[provider];   for (const candidate of models) {    const normalized = normalizeModelSlug(candidate, provider);    if (      !normalized ||      normalized.length > MAX_CUSTOM_MODEL_LENGTH ||      builtInModelSlugs.has(normalized) ||      seen.has(normalized)    ) {      continue;    }     seen.add(normalized);    normalizedModels.push(normalized);    if (normalizedModels.length >= MAX_CUSTOM_MODEL_COUNT) {      break;    }  }   return normalizedModels;} function normalizeAppSettings(settings: AppSettings): AppSettings {  return {    ...settings,    customCodexModels: normalizeCustomModelSlugs(settings.customCodexModels, "codex"),  };} export function getAppModelOptions(  provider: ProviderKind,  customModels: readonly string[],  selectedModel?: string | null,): AppModelOption[] {  const options: AppModelOption[] = getModelOptions(provider).map(({ slug, name }) => ({    slug,    name,    isCustom: false,  }));  const seen = new Set(options.map((option) => option.slug));   for (const slug of normalizeCustomModelSlugs(customModels, provider)) {    if (seen.has(slug)) {      continue;    }     seen.add(slug);    options.push({      slug,      name: slug,      isCustom: true,    });  }   const normalizedSelectedModel = normalizeModelSlug(selectedModel, provider);  if (normalizedSelectedModel && !seen.has(normalizedSelectedModel)) {    options.push({      slug: normalizedSelectedModel,      name: normalizedSelectedModel,      isCustom: true,    });  }   return options;} export function resolveAppModelSelection(  provider: ProviderKind,  customModels: readonly string[],  selectedModel: string | null | undefined,): string {  const options = getAppModelOptions(provider, customModels, selectedModel);  const trimmedSelectedModel = selectedModel?.trim();  if (trimmedSelectedModel) {    const direct = options.find((option) => option.slug === trimmedSelectedModel);    if (direct) {      return direct.slug;    }     const byName = options.find(      (option) => option.name.toLowerCase() === trimmedSelectedModel.toLowerCase(),    );    if (byName) {      return byName.slug;    }  }   const normalizedSelectedModel = normalizeModelSlug(selectedModel, provider);  if (!normalizedSelectedModel) {    return getDefaultModel(provider);  }   return (    options.find((option) => option.slug === normalizedSelectedModel)?.slug ??    getDefaultModel(provider)  );} export function getSlashModelOptions(  provider: ProviderKind,  customModels: readonly string[],  query: string,  selectedModel?: string | null,): AppModelOption[] {  const normalizedQuery = query.trim().toLowerCase();  const options = getAppModelOptions(provider, customModels, selectedModel);  if (!normalizedQuery) {    return options;  }   return options.filter((option) => {    const searchSlug = option.slug.toLowerCase();    const searchName = option.name.toLowerCase();    return searchSlug.includes(normalizedQuery) || searchName.includes(normalizedQuery);  });} function emitChange(): void {  for (const listener of listeners) {    listener();  }} function parsePersistedSettings(value: string | null): AppSettings {  if (!value) {    return DEFAULT_APP_SETTINGS;  }   try {    return normalizeAppSettings(Schema.decodeSync(Schema.fromJsonString(AppSettingsSchema))(value));  } catch {    return DEFAULT_APP_SETTINGS;  }} export function getAppSettingsSnapshot(): AppSettings {  if (typeof window === "undefined") {    return DEFAULT_APP_SETTINGS;  }   const raw = window.localStorage.getItem(APP_SETTINGS_STORAGE_KEY);  if (raw === cachedRawSettings) {    return cachedSnapshot;  }   cachedRawSettings = raw;  cachedSnapshot = parsePersistedSettings(raw);  return cachedSnapshot;} function persistSettings(next: AppSettings): void {  if (typeof window === "undefined") return;   const raw = JSON.stringify(next);  try {    if (raw !== cachedRawSettings) {      window.localStorage.setItem(APP_SETTINGS_STORAGE_KEY, raw);    }  } catch {    // Best-effort persistence only.  }   cachedRawSettings = raw;  cachedSnapshot = next;} function subscribe(listener: () => void): () => void {  listeners.push(listener);   const onStorage = (event: StorageEvent) => {    if (event.key === APP_SETTINGS_STORAGE_KEY) {      emitChange();    }  };   window.addEventListener("storage", onStorage);  return () => {    listeners = listeners.filter((entry) => entry !== listener);    window.removeEventListener("storage", onStorage);  };} export function useAppSettings() {  const settings = useSyncExternalStore(    subscribe,    getAppSettingsSnapshot,    () => DEFAULT_APP_SETTINGS,  );   const updateSettings = useCallback((patch: Partial<AppSettings>) => {    const next = normalizeAppSettings(      Schema.decodeSync(AppSettingsSchema)({        ...getAppSettingsSnapshot(),        ...patch,      }),    );    persistSettings(next);    emitChange();  }, []);   const resetSettings = useCallback(() => {    persistSettings(DEFAULT_APP_SETTINGS);    emitChange();  }, []);   return {    settings,    updateSettings,    resetSettings,    defaults: DEFAULT_APP_SETTINGS,  } as const;}