176 lines · 6.4 KB · TypeScript
RawDownload
/** * Shared server settings. * * Every server keeps its own `settings.json`, but some keys are user * preferences that only live on the server because the server has to act on * them (auto-settlement runs with no client attached). A user does not want * those to differ per machine. Clients write these keys to every shared-settings * sync target, and warn when another target still holds a different value so * the user can push their current value out. */import type {  EnvironmentId,  ExecutionEnvironmentCapabilities,  ServerSettings,  ServerSettingsPatch,} from "@t3tools/contracts";import { isModelSelectionProviderEnabled } from "@t3tools/shared/serverSettings";import * as Equal from "effect/Equal";import * as Struct from "effect/Struct"; import type { EnvironmentConnectionPhase } from "../connection/presentation.ts"; /** Server keys that hold a user preference rather than machine config. */const SHARED_SERVER_SETTING_KEYS = [  "continueThreadsAfterServerUpdate",  "sidebarAutoSettleAfterDays",  "sidebarAutoSettleOnMerge",  "newWorktreesStartFromOrigin",  "sourceControlWritingStyle",  "textGenerationModelSelection",] as const satisfies ReadonlyArray<keyof ServerSettings & keyof ServerSettingsPatch>; export type SharedServerSettingKey = (typeof SHARED_SERVER_SETTING_KEYS)[number]; const SHARED_KEY_SET = new Set<string>(SHARED_SERVER_SETTING_KEYS); /** Split a server patch into the keys every environment should receive and the primary-only rest. */export function splitSharedServerPatch(patch: ServerSettingsPatch): {  sharedPatch: ServerSettingsPatch;  localPatch: ServerSettingsPatch;} {  const sharedPatch: Record<string, unknown> = {};  const localPatch: Record<string, unknown> = {};  for (const [key, value] of Object.entries(patch)) {    if (SHARED_KEY_SET.has(key)) {      sharedPatch[key] = value;    } else {      localPatch[key] = value;    }  }  return {    sharedPatch: sharedPatch as ServerSettingsPatch,    localPatch: localPatch as ServerSettingsPatch,  };} /** Filter unsupported preferences; direct model writes retain the server's fallback behavior. */export function filterSharedServerPatch(  patch: ServerSettingsPatch,  capabilities: Pick<ExecutionEnvironmentCapabilities, "threadRestartContinuation"> | undefined,  settings?: ServerSettings,  sourceSettings = settings,  targetIsSource = false,): ServerSettingsPatch {  const instanceId =    patch.textGenerationModelSelection?.instanceId ??    sourceSettings?.textGenerationModelSelection.instanceId;  if (    !targetIsSource &&    patch.textGenerationModelSelection &&    (!settings ||      (instanceId !== undefined &&        (sourceSettings?.providerInstances[instanceId]?.driver ?? instanceId) !==          (settings.providerInstances[instanceId]?.driver ?? instanceId)) ||      !isModelSelectionProviderEnabled(settings, {        ...settings.textGenerationModelSelection,        ...patch.textGenerationModelSelection,      }))  ) {    patch = Struct.omit(patch, ["textGenerationModelSelection"]);  }  return capabilities?.threadRestartContinuation === true    ? patch    : Struct.omit(patch, ["continueThreadsAfterServerUpdate"]);} /** The shared subset supported by one environment. */export function pickSharedServerSettings(  settings: ServerSettings,  capabilities?: Pick<ExecutionEnvironmentCapabilities, "threadRestartContinuation">,): ServerSettingsPatch {  return filterSharedServerPatch(    Struct.pick(settings, SHARED_SERVER_SETTING_KEYS),    capabilities,    settings,  );} /** * Whether an environment can participate in shared-settings sync right now. * Auto-settlement establishes baseline support; newer preferences are filtered separately. */export function supportsSharedSettingsSync(environment: {  readonly connection: { readonly phase: EnvironmentConnectionPhase };  readonly serverConfig: {    readonly environment: {      readonly capabilities: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlement">;    };  } | null;}): boolean {  return (    environment.connection.phase === "connected" &&    environment.serverConfig?.environment.capabilities.threadAutoSettlement === true  );} export interface SharedSettingsEnvironment {  readonly environmentId: EnvironmentId;  readonly label: string;  readonly syncEligible: boolean;  readonly settings: ServerSettings | null;  readonly capabilities?:    | Pick<ExecutionEnvironmentCapabilities, "threadRestartContinuation">    | undefined;} /** * Shared-settings sync targets whose values differ from the primary * environment's. Other environments are skipped: nothing can be read from or * written to them, or their server lacks baseline shared-settings support. With no * primary settings loaded there is nothing to compare against, so nothing is * reported. Callers must pass the real loaded settings, never a default * fallback, or "apply to all" would push defaults over real values. */export function findSharedSettingsMismatches(input: {  readonly primaryEnvironmentId: EnvironmentId | null;  readonly primarySettings: ServerSettings | null;  readonly primaryCapabilities?:    | Pick<ExecutionEnvironmentCapabilities, "threadRestartContinuation">    | undefined;  readonly environments: ReadonlyArray<SharedSettingsEnvironment>;}): ReadonlyArray<{ readonly environmentId: EnvironmentId; readonly label: string }> {  if (input.primaryEnvironmentId === null || input.primarySettings === null) {    return [];  }  const primarySettings = pickSharedServerSettings(    input.primarySettings,    input.primaryCapabilities,  );  return input.environments.flatMap((environment) => {    if (      environment.environmentId === input.primaryEnvironmentId ||      !environment.syncEligible ||      environment.settings === null    ) {      return [];    }    const expected = filterSharedServerPatch(      primarySettings,      environment.capabilities,      environment.settings,      input.primarySettings ?? undefined,    );    let actual = filterSharedServerPatch(      pickSharedServerSettings(environment.settings, environment.capabilities),      input.primaryCapabilities,      environment.settings,    );    if (!expected.textGenerationModelSelection) {      actual = Struct.omit(actual, ["textGenerationModelSelection"]);    }    return Equal.equals(actual, expected)      ? []      : [{ environmentId: environment.environmentId, label: environment.label }];  });}