864 lines · 36.1 KB · TypeScript
RawDownload
import * as Effect from "effect/Effect";import * as Duration from "effect/Duration";import * as Schema from "effect/Schema";import * as SchemaTransformation from "effect/SchemaTransformation";import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts";import { ThreadEnvMode } from "./environment.ts";import {  DEFAULT_TEXT_GENERATION_MODEL,  DEFAULT_TEXT_GENERATION_REASONING_EFFORT,  ProviderOptionSelections,} from "./model.ts";import { ModelSelection } from "./orchestration.ts";import {  DEFAULT_PREVIEW_APPEARANCE,  DEFAULT_PREVIEW_ZOOM_FACTOR,  FILL_PREVIEW_VIEWPORT,  PreviewAppearancePreference,  PreviewViewportSetting,  PreviewZoomFactor,} from "./preview.ts";import { ProviderInstanceConfig, ProviderInstanceId } from "./providerInstance.ts"; // ── Client Settings (local-only) ─────────────────────────────── export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);export type TimestampFormat = typeof TimestampFormat.Type;export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale"; export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at"; export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at"]);export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at"; export const SidebarProjectGroupingMode = Schema.Literals([  "repository",  "repository_path",  "separate",]);export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type;export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository";export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1;export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15;export const SidebarThreadPreviewCount = Schema.Int.check(  Schema.isBetween({    minimum: MIN_SIDEBAR_THREAD_PREVIEW_COUNT,    maximum: MAX_SIDEBAR_THREAD_PREVIEW_COUNT,  }),);export type SidebarThreadPreviewCount = typeof SidebarThreadPreviewCount.Type;export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6;export const MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS = 1;export const MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS = 90;export const SidebarAutoSettleAfterDays = Schema.Number.check(  Schema.isBetween({    minimum: MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,    maximum: MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,  }),);export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type;export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3;export const MIN_GLASS_OPACITY = 40;export const MAX_GLASS_OPACITY = 100;export const GlassOpacity = Schema.Int.check(  Schema.isBetween({    minimum: MIN_GLASS_OPACITY,    maximum: MAX_GLASS_OPACITY,  }),);export type GlassOpacity = typeof GlassOpacity.Type;export const DEFAULT_GLASS_OPACITY: GlassOpacity = 80;/** * Font size preferences, in CSS pixels. The ranges are deliberately narrow: * the interface size scales every rem-based dimension in the app, so the * bounds keep layouts intact rather than offering unusable extremes. */export const MIN_INTERFACE_FONT_SIZE = 12;export const MAX_INTERFACE_FONT_SIZE = 20;export const InterfaceFontSize = Schema.Int.check(  Schema.isBetween({ minimum: MIN_INTERFACE_FONT_SIZE, maximum: MAX_INTERFACE_FONT_SIZE }),);export type InterfaceFontSize = typeof InterfaceFontSize.Type;export const DEFAULT_INTERFACE_FONT_SIZE: InterfaceFontSize = 16; export const MIN_PROMPT_FONT_SIZE = 12;export const MAX_PROMPT_FONT_SIZE = 20;export const PromptFontSize = Schema.Int.check(  Schema.isBetween({ minimum: MIN_PROMPT_FONT_SIZE, maximum: MAX_PROMPT_FONT_SIZE }),);export type PromptFontSize = typeof PromptFontSize.Type;export const DEFAULT_PROMPT_FONT_SIZE: PromptFontSize = 14; export const MIN_CODE_FONT_SIZE = 10;export const MAX_CODE_FONT_SIZE = 18;export const CodeFontSize = Schema.Int.check(  Schema.isBetween({ minimum: MIN_CODE_FONT_SIZE, maximum: MAX_CODE_FONT_SIZE }),);export type CodeFontSize = typeof CodeFontSize.Type;export const DEFAULT_CODE_FONT_SIZE: CodeFontSize = 13; export const MIN_TERMINAL_FONT_SIZE = 8;export const MAX_TERMINAL_FONT_SIZE = 20;export const TerminalFontSize = Schema.Int.check(  Schema.isBetween({ minimum: MIN_TERMINAL_FONT_SIZE, maximum: MAX_TERMINAL_FONT_SIZE }),);export type TerminalFontSize = typeof TerminalFontSize.Type;export const DEFAULT_TERMINAL_FONT_SIZE: TerminalFontSize = 12; export const EnvironmentIdentificationMode = Schema.Literals(["artwork", "pill", "none"]);export type EnvironmentIdentificationMode = typeof EnvironmentIdentificationMode.Type;export const DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE: EnvironmentIdentificationMode = "artwork"; /** * A user-chosen font family (a single name or a comma-separated list). Empty * means "use the app default"; clients compose their own fallback stacks. */export const FontFamilyPreference = Schema.String.check(Schema.isMaxLength(200));export type FontFamilyPreference = typeof FontFamilyPreference.Type; /** * Defaults for the in-app preview browser, applied whenever a tab is opened * without an explicit viewport/zoom/appearance — by the user opening a browser * tab, or by an agent calling `preview_open` with no size. Client-local * because the Chromium guest they configure is desktop-local. */export const DEFAULT_BROWSER_VIEWPORT: PreviewViewportSetting = FILL_PREVIEW_VIEWPORT;export const DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW = true; export const ClientSettingsSchema = Schema.Struct({  browserDefaultViewport: PreviewViewportSetting.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_VIEWPORT)),  ),  browserDefaultZoomFactor: PreviewZoomFactor.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_PREVIEW_ZOOM_FACTOR)),  ),  browserDefaultAppearance: PreviewAppearancePreference.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_PREVIEW_APPEARANCE)),  ),  /**   * Whether an agent opening a preview pops the floating mini player into   * view. Only applies when the agent didn't ask either way — an explicit   * `open`/`show` on `preview_open` still wins, since that is the agent   * deliberately showing or hiding its work.   */  browserAutoShowFloatingPreview: Schema.Boolean.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW)),  ),  // Desktop-only: require holding the quit shortcut (Cmd/Ctrl+Q) before the  // app quits; a quick tap only shows a hint. Browser clients ignore it.  confirmQuit: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),  confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),  confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),  dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe(    Schema.withDecodingDefault(Effect.succeed([])),  ),  diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),  environmentIdentificationMode: EnvironmentIdentificationMode.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_ENVIRONMENT_IDENTIFICATION_MODE)),  ),  glassOpacity: GlassOpacity.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)),  ),  fontSizeInterface: InterfaceFontSize.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_INTERFACE_FONT_SIZE)),  ),  fontSizePrompt: PromptFontSize.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROMPT_FONT_SIZE)),  ),  fontSizeCode: CodeFontSize.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_CODE_FONT_SIZE)),  ),  fontSizeTerminal: TerminalFontSize.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_TERMINAL_FONT_SIZE)),  ),  fontFamilyCode: FontFamilyPreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))),  fontFamilyComposer: FontFamilyPreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))),  fontFamilySans: FontFamilyPreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))),  fontFamilyTerminal: FontFamilyPreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))),  // Grayscale `-webkit-font-smoothing: antialiased` (thinner strokes);  // disabling restores the platform's heavier default. No effect off macOS.  fontSmoothing: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),  // Model favorites. Historically keyed by provider kind, now  // widened to `ProviderInstanceId` so users can favorite a specific model  // on a custom provider instance (e.g. "Codex Personal · gpt-5") without  // the UI collapsing it into the same bucket as the default Codex. The  // widening is backward-compatible by construction: prior provider-kind  // strings satisfy the `ProviderInstanceId` slug schema, so previously  // persisted favorites decode unchanged and continue to point at the  // default instance for their kind (because `defaultInstanceIdForDriver(kind)`  // uses the same slug). The field name is kept as `provider` for storage  // stability; new call sites should treat the value as an instance id.  favorites: Schema.Array(    Schema.Struct({      provider: ProviderInstanceId,      model: TrimmedNonEmptyString,    }),  ).pipe(Schema.withDecodingDefault(Effect.succeed([]))),  providerModelPreferences: Schema.Record(    ProviderInstanceId,    Schema.Struct({      hiddenModels: Schema.Array(Schema.String).pipe(        Schema.withDecodingDefault(Effect.succeed([])),      ),      modelOrder: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),    }),  ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),  // Legacy plan mode. The composer's Build/Plan toggle was removed from the  // default UI; this beta flag restores it (plus the /plan and /default slash  // commands) for users who still rely on the old workflow.  planModeEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),  // Legacy sidebar (the original per-project tree). Deliberately a fresh key  // (was `sidebarV2Enabled` + `sidebarV2ConfiguredByUser`): decoding drops the  // old keys, so everyone, including prior beta opt-outs, resets to the new  // default sidebar.  legacySidebarEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),  sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)),  ),  sidebarAutoSettleOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),  sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)),  ),  sidebarProjectGroupingOverrides: Schema.Record(    TrimmedNonEmptyString,    SidebarProjectGroupingMode,  ).pipe(Schema.withDecodingDefault(Effect.succeed({}))),  sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_SORT_ORDER)),  ),  sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)),  ),  sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),  ),  timestampFormat: TimestampFormat.pipe(    Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),  ),  wordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),});export type ClientSettings = typeof ClientSettingsSchema.Type; export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientSettingsSchema)({}); // ── Server Settings (server-authoritative) ──────────────────── // Moved to environment.ts so orchestration contracts can use it without an// import cycle; re-exported here for compatibility with deep imports.export { ThreadEnvMode } from "./environment.ts"; const makeBinaryPathSetting = (fallback: string) =>  TrimmedString.pipe(    Schema.decodeTo(      Schema.String,      SchemaTransformation.transformOrFail({        decode: (value) => Effect.succeed(value || fallback),        encode: (value) => Effect.succeed(value),      }),    ),    Schema.withDecodingDefault(Effect.succeed(fallback)),  ); export type ProviderSettingsFormControl = "text" | "password" | "textarea" | "switch"; export interface ProviderSettingsFormAnnotation {  readonly control?: ProviderSettingsFormControl | undefined;  readonly placeholder?: string | undefined;  readonly hidden?: boolean | undefined;  readonly clearWhenEmpty?: "omit" | "persist" | undefined;} export interface ProviderSettingsFormSchemaAnnotation {  readonly order?: readonly string[] | undefined;} declare module "effect/Schema" {  namespace Annotations {    interface Annotations {      readonly providerSettingsForm?: ProviderSettingsFormAnnotation | undefined;      readonly providerSettingsFormSchema?: ProviderSettingsFormSchemaAnnotation | undefined;    }  }} export type ProviderSettingsOrder<Fields extends Schema.Struct.Fields> = readonly Extract<  keyof Fields,  string>[]; export function makeProviderSettingsSchema<const Fields extends Schema.Struct.Fields>(  fields: Fields,  options?: {    readonly order?: ProviderSettingsOrder<Fields> | undefined;  },): Schema.Struct<Fields> {  return Schema.Struct(fields).pipe(    Schema.annotate({      providerSettingsFormSchema:        options?.order === undefined ? undefined : { order: options.order },    }),  );} export const CodexSettings = makeProviderSettingsSchema(  {    enabled: Schema.Boolean.pipe(      Schema.withDecodingDefault(Effect.succeed(true)),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    binaryPath: makeBinaryPathSetting("codex").pipe(      Schema.annotateKey({        title: "Binary path",        description: "Path to the Codex binary used by this instance.",        providerSettingsForm: { placeholder: "codex", clearWhenEmpty: "omit" },      }),    ),    homePath: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "CODEX_HOME path",        description: "Custom Codex home and config directory.",        providerSettingsForm: {          placeholder: "~/.codex",          clearWhenEmpty: "omit",        },      }),    ),    shadowHomePath: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "Shadow home path",        description:          "Account-specific Codex home. Keeps auth.json separate while sharing state from CODEX_HOME.",        providerSettingsForm: {          placeholder: "~/.codex-t3/personal",          clearWhenEmpty: "omit",        },      }),    ),    launchArgs: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "Launch arguments",        description: "Additional CLI arguments passed to codex app-server on session start.",      }),    ),    customModels: Schema.Array(Schema.String).pipe(      Schema.withDecodingDefault(Effect.succeed([])),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),  },  {    order: ["binaryPath", "homePath", "shadowHomePath", "launchArgs"],  },);export type CodexSettings = typeof CodexSettings.Type; export const ClaudeSettings = makeProviderSettingsSchema(  {    enabled: Schema.Boolean.pipe(      Schema.withDecodingDefault(Effect.succeed(true)),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    binaryPath: makeBinaryPathSetting("claude").pipe(      Schema.annotateKey({        title: "Binary path",        description: "Path to the Claude binary used by this instance.",        providerSettingsForm: { placeholder: "claude", clearWhenEmpty: "omit" },      }),    ),    homePath: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "CLAUDE_CONFIG_DIR path",        description:          "Custom Claude home and config directory. Keeps .claude.json and .claude separate.",        providerSettingsForm: { placeholder: "~/.claude", clearWhenEmpty: "omit" },      }),    ),    customModels: Schema.Array(Schema.String).pipe(      Schema.withDecodingDefault(Effect.succeed([])),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    launchArgs: Schema.String.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "Launch arguments",        description: "Additional CLI arguments passed on session start.",        providerSettingsForm: {          placeholder: "e.g. --chrome",          clearWhenEmpty: "omit",        },      }),    ),  },  {    order: ["binaryPath", "homePath", "launchArgs"],  },);export type ClaudeSettings = typeof ClaudeSettings.Type; export const CursorSettings = makeProviderSettingsSchema(  {    enabled: Schema.Boolean.pipe(      Schema.withDecodingDefault(Effect.succeed(false)),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    binaryPath: makeBinaryPathSetting("cursor-agent").pipe(      Schema.annotateKey({        title: "Binary path",        description: "Path to the Cursor agent binary.",        providerSettingsForm: { placeholder: "cursor-agent", clearWhenEmpty: "omit" },      }),    ),    apiEndpoint: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "API endpoint",        description: "Override the Cursor API endpoint for this instance.",        providerSettingsForm: {          placeholder: "https://...",          clearWhenEmpty: "omit",        },      }),    ),    customModels: Schema.Array(Schema.String).pipe(      Schema.withDecodingDefault(Effect.succeed([])),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),  },  {    order: ["binaryPath", "apiEndpoint"],  },);export type CursorSettings = typeof CursorSettings.Type; export const GrokSettings = makeProviderSettingsSchema(  {    enabled: Schema.Boolean.pipe(      Schema.withDecodingDefault(Effect.succeed(true)),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    binaryPath: makeBinaryPathSetting("grok").pipe(      Schema.annotateKey({        title: "Binary path",        description: "Path to the Grok CLI binary.",        providerSettingsForm: { placeholder: "grok", clearWhenEmpty: "omit" },      }),    ),    customModels: Schema.Array(Schema.String).pipe(      Schema.withDecodingDefault(Effect.succeed([])),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),  },  {    order: ["binaryPath"],  },);export type GrokSettings = typeof GrokSettings.Type; export const OpenCodeSettings = makeProviderSettingsSchema(  {    enabled: Schema.Boolean.pipe(      Schema.withDecodingDefault(Effect.succeed(true)),      Schema.annotateKey({ providerSettingsForm: { hidden: true } }),    ),    binaryPath: makeBinaryPathSetting("opencode").pipe(      Schema.annotateKey({        title: "Binary path",        description: "Path to the OpenCode binary.",        providerSettingsForm: {          placeholder: "opencode",          clearWhenEmpty: "omit",        },      }),    ),    serverUrl: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "Server URL",        description: "Leave blank to let T3 Code spawn the server when needed.",        providerSettingsForm: {          placeholder: "http://127.0.0.1:4096",          clearWhenEmpty: "omit",        },      }),    ),    serverPassword: TrimmedString.pipe(      Schema.withDecodingDefault(Effect.succeed("")),      Schema.annotateKey({        title: "Server password",        description: "Stored in plain text on disk.",        providerSettingsForm: {          control: "password",          placeholder: "Optional",          clearWhenEmpty: "omit",        },      }),

Lines 1–500 of 864.