import { SshDeviceHostConfigs } from "./device.ts"; 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 { ForwardCompatibleNullable, ProjectId, TrimmedNonEmptyString, TrimmedString, } from "./baseSchemas.ts"; import { UsageLimitSourceId } from "./usageLimitSourceId.ts"; import { EnvironmentMachineKind, ThreadEnvMode } from "./environment.ts"; import { KeybindingShortcut } from "./keybindings.ts"; import { CustomModelSetting, DEFAULT_TEXT_GENERATION_MODEL, DEFAULT_TEXT_GENERATION_REASONING_EFFORT, ProviderOptionSelections, } from "./model.ts"; import { ModelSelection, ProjectScript } from "./orchestration.ts"; import { BrowserProfile, BrowserProfileId, DEFAULT_BROWSER_PROFILE_ID } from "./browserProfile.ts"; import { DEFAULT_PREVIEW_APPEARANCE, DEFAULT_PREVIEW_ZOOM_FACTOR, FILL_PREVIEW_VIEWPORT, PreviewAppearancePreference, PreviewViewportSetting, PreviewZoomFactor, } from "./preview.ts"; import { ProviderInstanceConfig, ProviderInstanceId, type ProviderDriverKind, } from "./providerInstance.ts"; import { PullRequestMergeMethod } from "./pullRequest.ts"; // ── Client Settings (local-only) ─────────────────────────────── export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]); export type TimestampFormat = typeof TimestampFormat.Type; const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale"; export const DiffLayout = Schema.Literals(["stacked", "split"]); export type DiffLayout = typeof DiffLayout.Type; const DEFAULT_DIFF_LAYOUT: DiffLayout = "stacked"; 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; 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; 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; 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; const DEFAULT_GLASS_OPACITY: GlassOpacity = 80; export const MIN_APPEARANCE_CONTRAST = 50; export const MAX_APPEARANCE_CONTRAST = 200; export const AppearanceContrast = Schema.Int.check( Schema.isBetween({ minimum: MIN_APPEARANCE_CONTRAST, maximum: MAX_APPEARANCE_CONTRAST }), ); export type AppearanceContrast = typeof AppearanceContrast.Type; const DEFAULT_APPEARANCE_CONTRAST: AppearanceContrast = 100; export const MIN_PANEL_ANIMATION_DURATION_MS = 0; export const MAX_PANEL_ANIMATION_DURATION_MS = 400; export const PanelAnimationDurationMs = Schema.Int.check( Schema.isBetween({ minimum: MIN_PANEL_ANIMATION_DURATION_MS, maximum: MAX_PANEL_ANIMATION_DURATION_MS, }), ); export type PanelAnimationDurationMs = typeof PanelAnimationDurationMs.Type; const DEFAULT_PANEL_ANIMATION_DURATION_MS: PanelAnimationDurationMs = 0; /** * 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; 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"; export const SnapShotKeyChord = KeybindingShortcut.check( Schema.makeFilter( (shortcut) => shortcut.metaKey || shortcut.ctrlKey || shortcut.shiftKey || shortcut.altKey || shortcut.modKey || "Snapshot shortcut requires a modifier.", ), ); export type SnapShotKeyChord = typeof SnapShotKeyChord.Type; export const SNAP_SHOT_MODIFIERS = ["shift", "meta", "control", "alt"] as const; export const SnapShotModifier = Schema.Literals(SNAP_SHOT_MODIFIERS); export type SnapShotModifier = typeof SnapShotModifier.Type; export const SnapShotShortcut = Schema.Union([ Schema.Struct({ kind: Schema.Literal("both-shift-keys") }), Schema.Struct({ kind: Schema.Literal("modifier-pair"), modifier: SnapShotModifier }), SnapShotKeyChord, ]); export type SnapShotShortcut = typeof SnapShotShortcut.Type; export const SnapShotSound = Schema.Literals(["soft-pop", "camera-shutter"]); export type SnapShotSound = typeof SnapShotSound.Type; const DEFAULT_SNAP_SHOT_SOUND: SnapShotSound = "soft-pop"; export type SnapShotModifierPairShortcut = Extract; export function isModifierPairShortcut( shortcut: SnapShotShortcut, ): shortcut is SnapShotModifierPairShortcut { return "kind" in shortcut; } export function snapShotShortcutModifierPair( shortcut: SnapShotModifierPairShortcut, ): SnapShotModifier { return shortcut.kind === "both-shift-keys" ? "shift" : shortcut.modifier; } const APPLE_MODIFIER_LABELS: Record = { shift: "Shift", meta: "Command", control: "Control", alt: "Option", }; const OTHER_MODIFIER_LABELS: Record = { shift: "Shift", meta: "Super", control: "Ctrl", alt: "Alt", }; export function snapShotModifierPairLabel(modifier: SnapShotModifier, apple: boolean): string { const label = (apple ? APPLE_MODIFIER_LABELS : OTHER_MODIFIER_LABELS)[modifier]; return `${label} + ${label}`; } const DEFAULT_SNAP_SHOT_SHORTCUT: SnapShotShortcut = { kind: "both-shift-keys", }; export const QuitConfirmationMode = Schema.Literals(["direct", "hold", "double-click"]); export type QuitConfirmationMode = typeof QuitConfirmationMode.Type; const DEFAULT_QUIT_CONFIRMATION_MODE: QuitConfirmationMode = "hold"; const LegacyConfirmQuit = Schema.Boolean.pipe( Schema.decodeTo( QuitConfirmationMode, SchemaTransformation.transform({ decode: (confirmQuit): QuitConfirmationMode => (confirmQuit ? "hold" : "direct"), encode: (mode) => mode === "hold", }), ), ); const QuitConfirmationModeSetting = Schema.Union([QuitConfirmationMode, LegacyConfirmQuit]); /** * 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; /** * The environment's theme, set with `t3 theme set `. Each client applies * it once per value — live when connected, on its next connect otherwise — so * setting it switches every client, while a theme a user picks in Settings * afterwards sticks until the next set. Empty means "no environment theme", * which is also how it is cleared. */ export const DefaultThemePreference = Schema.String.check(Schema.isMaxLength(64)); // Deliberately absent from ServerSettingsPatch: `t3 theme set` checks that an // id is syntactically valid and actually resolvable, and a generic RPC patch // would let a client write a theme no client can resolve, bypassing both. export type DefaultThemePreference = typeof DefaultThemePreference.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. Recording quality is * client-local for the same reason: the Chromium guest being captured belongs * to the desktop app. */ export const DEFAULT_BROWSER_VIEWPORT: PreviewViewportSetting = FILL_PREVIEW_VIEWPORT; export const DEFAULT_BROWSER_AUTO_SHOW_FLOATING_PREVIEW = true; export const BROWSER_RECORDING_FRAME_RATES = [30, 60] as const; export const BrowserRecordingFrameRate = Schema.Literals(BROWSER_RECORDING_FRAME_RATES); export type BrowserRecordingFrameRate = typeof BrowserRecordingFrameRate.Type; export const DEFAULT_BROWSER_RECORDING_FRAME_RATE: BrowserRecordingFrameRate = 30; /** * Where a clicked link goes: the OS default browser, or a tab in the in-app * browser beside the thread. "system" is the default because that is what * every link did before the setting existed. */ export const BrowserLinkTarget = Schema.Literals(["system", "app"]); export type BrowserLinkTarget = typeof BrowserLinkTarget.Type; export const DEFAULT_BROWSER_LINK_TARGET: BrowserLinkTarget = "system"; export const LoadBalancingWeights = Schema.Record( TrimmedNonEmptyString, Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 100 })), ); export const DiffColorScheme = Schema.Literals(["red-green", "blue-orange"]); export const ClientSettingsSchema = Schema.Struct({ diffColorScheme: DiffColorScheme.pipe( Schema.withDecodingDefault(Effect.succeed("red-green" as const)), ), loadBalancingEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), loadBalancingWeights: LoadBalancingWeights.pipe(Schema.withDecodingDefault(Effect.succeed({}))), appearanceContrast: AppearanceContrast.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_APPEARANCE_CONTRAST)), ), // Panel motion defaults to zero because width and height transitions cause // layout work on every frame, which is noticeable on lower-power clients. panelAnimationDurationMs: PanelAnimationDurationMs.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PANEL_ANIMATION_DURATION_MS)), ), 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)), ), browserRecordingFrameRate: BrowserRecordingFrameRate.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_RECORDING_FRAME_RATE)), ), /** * Where links clicked in a thread (chat markdown, terminal output) open. * Only the desktop app has an in-app browser, so other clients ignore "app". */ browserLinkTarget: BrowserLinkTarget.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_LINK_TARGET)), ), /** * Whether an agent using 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)), ), /** * User-created browser profiles. The built-in Default and Incognito profiles * are synthesized by `resolveBrowserProfiles`, not stored here, so they * cannot be renamed away or deleted. */ browserProfiles: Schema.Array(BrowserProfile).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), /** Profile new tabs open under. Falls back to Default if it no longer exists. */ browserDefaultProfileId: BrowserProfileId.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_BROWSER_PROFILE_ID)), ), // Desktop-only. Boolean values from older settings files decode to their // equivalent mode and encode back as the canonical string value. confirmQuit: QuitConfirmationModeSetting.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_QUIT_CONFIRMATION_MODE)), ), confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), confirmThreadUnpin: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), dismissedProviderUpdateNotificationKeys: Schema.Array(TrimmedNonEmptyString).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), diffIgnoreWhitespace: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), diffLayout: DiffLayout.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_DIFF_LAYOUT))), 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))), // When the first-run welcome wizard finished (or was skipped), as an ISO // timestamp. `null` alone does not mean "show the wizard" — every install // that predates this field decodes to `null` — so the gate also requires an // empty workspace before it treats the client as a fresh install. onboardingCompletedAt: Schema.NullOr(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), // 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({}))), pullRequestMergeMethodOverrides: Schema.Record( TrimmedNonEmptyString, PullRequestMergeMethod, ).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 context window meter. The composer hides it by default; users who // still want the old usage indicator can restore it from Settings. contextWindowMeterEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), // Desktop resting composer: scrolling an existing thread's conversation // settles the composer into its single-line layout. Losing focus never does. composerCollapseOnScroll: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), proactivePanelsEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), showSkillsInSlashMenu: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), // 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))), 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)), ), snapShotEnabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), snapShotIncludeAccessibility: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(true)), ), snapShotShortcut: SnapShotShortcut.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SNAP_SHOT_SHORTCUT)), ), snapShotPlaySound: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), snapShotSound: SnapShotSound.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SNAP_SHOT_SOUND)), ), snapShotFlash: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), snapShotAnimations: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), 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) ──────────────────── const UsageModelTokenPrice = Schema.Number.check( Schema.isFinite(), Schema.isGreaterThanOrEqualTo(0), ); /** USD per million tokens. Omitted cache rates use the input rate. */ export const UsageModelPriceOverride = Schema.Struct({ inputCostPerMillionTokens: UsageModelTokenPrice, outputCostPerMillionTokens: UsageModelTokenPrice, cacheReadCostPerMillionTokens: Schema.optionalKey(UsageModelTokenPrice), cacheWriteCostPerMillionTokens: Schema.optionalKey(UsageModelTokenPrice), }); export type UsageModelPriceOverride = typeof UsageModelPriceOverride.Type; 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" | "select"; export interface ProviderSettingsFormOption { readonly value: string; readonly label: string; } export interface ProviderSettingsFormAnnotation { readonly control?: ProviderSettingsFormControl | undefined; readonly placeholder?: string | undefined; readonly hidden?: boolean | undefined; readonly clearWhenEmpty?: "omit" | "persist" | undefined; /** Choices for a `select` control. The first entry is the default. */ readonly options?: ReadonlyArray | 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 = readonly Extract< keyof Fields, string >[]; function makeProviderSettingsSchema( fields: Fields, options?: { readonly order?: ProviderSettingsOrder | undefined; }, ): Schema.Struct { 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(CustomModelSetting).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath", "homePath", "shadowHomePath", "launchArgs"], }, ); export type CodexSettings = typeof CodexSettings.Type; // Empty, or an integer from 100,000 to 1,000,000. Shared by the full // Claude settings schema and its patch so an out-of-range value fails at // the update that introduced it. const CLAUDE_AUTO_COMPACT_WINDOW_PATTERN = /^(?:|[1-9]\d{5}|1000000)$/; 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(CustomModelSetting).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", }, }), ), autoCompactWindow: TrimmedString.check( Schema.isPattern(CLAUDE_AUTO_COMPACT_WINDOW_PATTERN), ).pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ title: "Auto-compact after", description: "Compact after 100,000 to 1,000,000 tokens. Leave empty to use Claude's default.", providerSettingsForm: { placeholder: "e.g. 300000", clearWhenEmpty: "omit", }, }), ), }, { order: ["binaryPath", "homePath", "autoCompactWindow", "launchArgs"], }, ); export type ClaudeSettings = typeof ClaudeSettings.Type; export const CursorSettings = makeProviderSettingsSchema( { // Off by default like Grok and OpenCode. Users opt in from Settings. 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(CustomModelSetting).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath", "apiEndpoint"], }, ); export type CursorSettings = typeof CursorSettings.Type; export const GrokSettings = makeProviderSettingsSchema( { // Off by default (like Cursor and OpenCode): the binding is not yet // stable enough to probe on every install. Users opt in from Settings. enabled: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), 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(CustomModelSetting).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath"], }, ); export type GrokSettings = typeof GrokSettings.Type; /** * Antigravity ACP auth methods. Personal and Enterprise open a Google sign-in * in the browser. The API key and Agent Platform methods take credentials from * the instance config and never open a browser. */ export const ANTIGRAVITY_AUTH_METHODS = [ { value: "oauth-personal", label: "Google account" }, { value: "oauth-business", label: "Gemini Enterprise" }, { value: "gemini-api-key", label: "Gemini API key" }, { value: "agent-platform", label: "Agent Platform (Vertex AI)" }, ] as const satisfies ReadonlyArray; export const AntigravityAuthMethod = Schema.Literals( ANTIGRAVITY_AUTH_METHODS.map((method) => method.value), ); export type AntigravityAuthMethod = typeof AntigravityAuthMethod.Type; export const AntigravitySettings = makeProviderSettingsSchema( { enabled: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), authMethod: AntigravityAuthMethod.pipe( Schema.withDecodingDefault(Effect.succeed("oauth-personal" as const)), Schema.annotateKey({ title: "Sign-in method", description: "Google account uses your Antigravity subscription. Gemini Enterprise needs a GCP project and location. API key and Agent Platform bill the credential you enter.", providerSettingsForm: { control: "select", options: ANTIGRAVITY_AUTH_METHODS, clearWhenEmpty: "omit", }, }), ), apiKey: TrimmedString.pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ title: "API key", description: "Gemini API key, or a Vertex AI express key for Agent Platform. Stored in plain text on this environment.", providerSettingsForm: { control: "password", placeholder: "Optional", clearWhenEmpty: "omit", }, }), ), gcpProject: TrimmedString.pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ title: "GCP project", description: "Required for Gemini Enterprise. Agent Platform uses it when no API key is set.", providerSettingsForm: { placeholder: "my-project-id", clearWhenEmpty: "omit" }, }), ), gcpLocation: TrimmedString.pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ title: "GCP location", description: "Region for Gemini Enterprise or Agent Platform, such as us-central1.", providerSettingsForm: { placeholder: "us-central1", clearWhenEmpty: "omit" }, }), ), binaryPath: TrimmedString.pipe( Schema.withDecodingDefault(Effect.succeed("")), Schema.annotateKey({ title: "Binary path", description: "Optional path to the official Antigravity ACP executable. Leave empty for automatic selection.", providerSettingsForm: { placeholder: "Automatic", clearWhenEmpty: "persist" }, }), ), customModels: Schema.Array(CustomModelSetting).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["authMethod", "apiKey", "gcpProject", "gcpLocation", "binaryPath"] }, ); export type AntigravitySettings = typeof AntigravitySettings.Type; export const OpenCodeSettings = makeProviderSettingsSchema( { // Off by default (like Cursor and Grok): the binding is not yet stable // enough to probe on every install. Users opt in from Settings. enabled: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), 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", }, }), ), customModels: Schema.Array(CustomModelSetting).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath", "serverUrl", "serverPassword"], }, ); export type OpenCodeSettings = typeof OpenCodeSettings.Type; /** * A read-only quota source outside this environment's provider CLIs. The * only kind today is a CLIProxyAPI hub, whose management API reports the * windows of every pooled account. The key travels in settings for now, like * provider environment secrets; it is redacted before reaching a client. */ export const UsageLimitSourceConfig = Schema.Struct({ kind: Schema.Literal("cliproxy"), label: Schema.optional(TrimmedNonEmptyString), url: TrimmedNonEmptyString, managementKey: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), }); export type UsageLimitSourceConfig = typeof UsageLimitSourceConfig.Type; export const ObservabilitySettings = Schema.Struct({ otlpTracesUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), otlpMetricsUrl: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), }); export type ObservabilitySettings = typeof ObservabilitySettings.Type; export const SourceControlWritingStyleMode = Schema.Literals([ "repo_conventions", "conventional_commits", "custom", ]); export type SourceControlWritingStyleMode = typeof SourceControlWritingStyleMode.Type; export const SourceControlWritingStyleSettings = Schema.Struct({ mode: SourceControlWritingStyleMode.pipe( Schema.withDecodingDefault(Effect.succeed("repo_conventions" as const)), ), customInstructions: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), followChangeRequestTemplates: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(true)), ), }); export type SourceControlWritingStyleSettings = typeof SourceControlWritingStyleSettings.Type; export const DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL = Duration.seconds(30); export const DEFAULT_PROVIDER_HEALTH_REFRESH_INTERVAL = Duration.minutes(5); export const BackgroundActivityProfile = Schema.Literals([ "balanced", "performance", "battery-saver", ]); export type BackgroundActivityProfile = typeof BackgroundActivityProfile.Type; export const DEFAULT_BACKGROUND_ACTIVITY_PROFILE: BackgroundActivityProfile = "balanced"; export const BackgroundActivityProfileSelection = Schema.Literals([ "balanced", "performance", "battery-saver", "custom", ]); export type BackgroundActivityProfileSelection = typeof BackgroundActivityProfileSelection.Type; export const BackgroundActivityOverrides = Schema.Struct({ automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis), providerHealthRefreshInterval: Schema.optionalKey(Schema.DurationFromMillis), hostPowerMonitorActiveInterval: Schema.optionalKey(Schema.DurationFromMillis), hostPowerMonitorIdleInterval: Schema.optionalKey(Schema.DurationFromMillis), idleClientTtl: Schema.optionalKey(Schema.DurationFromMillis), pauseWhenHostLocked: Schema.optionalKey(Schema.Boolean), pauseWhenHostLowPower: Schema.optionalKey(Schema.Boolean), pauseWhenClientLowPower: Schema.optionalKey(Schema.Boolean), pauseWhenOnBattery: Schema.optionalKey(Schema.Boolean), }); export type BackgroundActivityOverrides = typeof BackgroundActivityOverrides.Type; export const BackgroundActivitySettings = Schema.Struct({ schemaVersion: Schema.Literal(1).pipe(Schema.withDecodingDefault(Effect.succeed(1 as const))), profile: BackgroundActivityProfileSelection.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_BACKGROUND_ACTIVITY_PROFILE)), ), baseProfile: Schema.optionalKey(BackgroundActivityProfile), overrides: BackgroundActivityOverrides.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }).pipe(Schema.withDecodingDefault(Effect.succeed({}))); export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; export const ServerSettings = Schema.Struct({ // Legacy token-by-token assistant output. Deliberately a fresh key (was // `enableAssistantStreaming`): decoding drops the old key, so everyone, // including prior opt-ins, resets to the buffered default. enableLegacyTokenStreaming: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), ), enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), // Retain the update-era key; recovery now needs an environment-owned opt-in. continueThreadsAfterServerUpdate: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(false)), ), /** * Whether agents may drive the in-app preview browser. Turning this off * withholds the MCP credential, so the `t3-code` server (and with it every * `preview_*` tool) is never attached to a provider session, and the prompt * text describing those tools is dropped along with them. The user's own * browser panel is unaffected — this gates agent access only. * * Server-authoritative rather than client-local: tool injection and prompt * construction both happen on the server, and the answer must not differ * between a desktop window and a phone attached to the same server. */ enableAgentBrowserAccess: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), projectAgentBrowserAccessOverrides: Schema.Record(ProjectId, Schema.Boolean).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), defaultAutoPull: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), defaultProjectScripts: Schema.Array(ProjectScript).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), projectScriptOverrides: Schema.Record(ProjectId, Schema.NullOr(Schema.Array(ProjectScript))).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), projectAutoPullOverrides: Schema.Record(ProjectId, Schema.Boolean).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), defaultModelSelection: Schema.NullOr(ModelSelection).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), /** * Whether agents may drive simulators and emulators. Gates the `device_*` * MCP tools and the preconfigured `agent-device` CLI the same way * `enableAgentBrowserAccess` gates the browser: server-authoritative, applied * when the provider session is prepared. The user's own Device panel is * unaffected. */ enableAgentDeviceAccess: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), /** * Whether this server may install and run T3's device helper processes. * Kept separate from agent access so enabling the user's Device panel does * not also grant providers control of simulators and emulators. */ enableDeviceSupport: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), /** Whether the server-local Device panel setup flow has been completed. */ deviceOnboardingCompleted: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), deviceHosts: SshDeviceHostConfigs.pipe(Schema.withDecodingDefault(Effect.succeed([]))), sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)), ), sidebarAutoSettleOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), backgroundActivity: BackgroundActivitySettings, // Legacy flat fields retained for old settings files and old clients. New // consumers should resolve `backgroundActivity` instead. automaticGitFetchInterval: Schema.DurationFromMillis.pipe( Schema.withDecodingDefault( Effect.succeed(Duration.toMillis(DEFAULT_AUTOMATIC_GIT_FETCH_INTERVAL)), ), ), providerHealthRefreshInterval: Schema.DurationFromMillis.pipe( Schema.withDecodingDefault( Effect.succeed(Duration.toMillis(DEFAULT_PROVIDER_HEALTH_REFRESH_INTERVAL)), ), ), backgroundActivityProfile: BackgroundActivityProfile.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_BACKGROUND_ACTIVITY_PROFILE)), ), defaultTheme: DefaultThemePreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))), /** * When the environment's theme was last set, so clients can tell a re-set * of the same value from one they already applied: `t3 theme set` must act * even when it names the theme it named before. Empty on environments * provisioned by builds that predate it, where clients fall back to * applying once per value. */ defaultThemeSetAt: Schema.String.check(Schema.isMaxLength(64)).pipe( Schema.withDecodingDefault(Effect.succeed("")), ), /** * The icon clients draw for this environment. Null means "use what the * server detected" (`environment.platform.machine`), falling back to a * generic server. Lives on the server, not the client, so every device * sees the same machine. A kind picked on a newer server decodes as null * here rather than failing the whole settings snapshot for an older client. */ environmentIcon: ForwardCompatibleNullable(EnvironmentMachineKind).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), defaultThreadEnvMode: ThreadEnvMode.pipe( Schema.withDecodingDefault(Effect.succeed("local" as const satisfies ThreadEnvMode)), ), newWorktreesStartFromOrigin: Schema.Boolean.pipe( Schema.withDecodingDefault(Effect.succeed(true)), ), addProjectBaseDirectory: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), textGenerationModelSelection: ModelSelection.pipe( Schema.withDecodingDefault( Effect.succeed({ instanceId: ProviderInstanceId.make("codex"), model: DEFAULT_TEXT_GENERATION_MODEL, options: [ { id: "reasoningEffort", value: DEFAULT_TEXT_GENERATION_REASONING_EFFORT, }, ], }), ), ), sourceControlWritingStyle: SourceControlWritingStyleSettings.pipe( Schema.withDecodingDefault(Effect.succeed({})), ), sourceControlWriterModelSelection: Schema.NullOr(ModelSelection).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), // Legacy single-instance-per-driver settings. Continues to be the source // of truth until `providerInstances` (below) lands per-driver migration // shims and the server starts hydrating instances from it. Driver-specific // schemas live here for the duration of the migration; once each driver // owns its config in its own package, this struct shrinks to nothing and // is removed entirely. providers: Schema.Struct({ codex: CodexSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), cursor: CursorSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), grok: GrokSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), opencode: OpenCodeSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), antigravity: AntigravitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), }).pipe(Schema.withDecodingDefault(Effect.succeed({}))), // New driver-agnostic instance map. Keyed by `ProviderInstanceId`; values // are `ProviderInstanceConfig` envelopes. The driver-specific config blob // is `Schema.Unknown` at this layer so envelopes with unknown drivers // (forks, downgrades, in-flight PR branches) round-trip without loss. // See providerInstance.ts for the forward/backward compatibility invariant. providerInstances: Schema.Record(ProviderInstanceId, ProviderInstanceConfig).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), observability: ObservabilitySettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), // Keyed by a user-chosen id so a source keeps its rows across edits. Entries // this build cannot decode round-trip untouched, as provider instances do. usageLimitSources: Schema.Record(UsageLimitSourceId, UsageLimitSourceConfig).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), /** Exact model IDs, applied to past and future usage on this environment. */ usagePriceOverrides: Schema.Record(TrimmedNonEmptyString, UsageModelPriceOverride).pipe( Schema.withDecodingDefault(Effect.succeed({})), ), }); export type ServerSettings = typeof ServerSettings.Type; export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({}); /** * Read the legacy `enabled` flag embedded in a provider instance config * blob. The envelope-level `ProviderInstanceConfig.enabled` is the single * flag going forward; this reader exists for legacy `providers.` * blobs and old settings files that still carry the flag in-config. */ export const providerInstanceConfigEnabledFlag = (config: unknown): boolean | undefined => { if (config === null || typeof config !== "object" || Array.isArray(config)) { return undefined; } const enabled = (config as { readonly enabled?: unknown }).enabled; return typeof enabled === "boolean" ? enabled : undefined; }; /** * Default enabled state for a built-in driver when neither the envelope nor * the config blob carries a flag. Derived from the driver's settings schema * through `DEFAULT_SERVER_SETTINGS`, so the schema's decoding default stays * the single source of truth. Unknown (fork) drivers default to enabled. */ const defaultEnabledForDriver = (driver: ProviderDriverKind): boolean => { const legacyDefaults = DEFAULT_SERVER_SETTINGS.providers as Record< string, { readonly enabled?: boolean } | undefined >; return legacyDefaults[driver]?.enabled ?? true; }; /** * Resolve whether a configured provider instance is enabled. An explicit * false on either the envelope or the in-config flag wins (most * restrictive), so a user's disable is never silently undone by the other * flag. Otherwise: envelope, then config, then the driver's default. */ export const resolveProviderInstanceEnabled = ( instance: Pick, ): boolean => { const configEnabled = providerInstanceConfigEnabledFlag(instance.config); if (instance.enabled === false || configEnabled === false) { return false; } return instance.enabled ?? configEnabled ?? defaultEnabledForDriver(instance.driver); }; export const ServerSettingsOperation = Schema.Literals([ "normalize", "check-exists", "read-file", "read-provider-history", "read-secret", "remove-secret", "remove-stale-secret", "write-secret", "write-file", "prepare-directory", ]); export type ServerSettingsOperation = typeof ServerSettingsOperation.Type; export class ServerSettingsError extends Schema.TaggedError()( "ServerSettingsError", { settingsPath: Schema.String, operation: ServerSettingsOperation, providerInstanceId: Schema.optional(Schema.String), environmentVariable: Schema.optional(Schema.String), cause: Schema.Defect(), }, ) { override get message(): string { const provider = this.providerInstanceId === undefined ? "" : ` for provider ${this.providerInstanceId}`; const variable = this.environmentVariable === undefined ? "" : ` and environment variable ${this.environmentVariable}`; return `Server settings ${this.operation} failed${provider}${variable} at ${this.settingsPath}.`; } } // ── Unified type ───────────────────────────────────────────────────── export type UnifiedSettings = ServerSettings & ClientSettings; export const DEFAULT_UNIFIED_SETTINGS: UnifiedSettings = { ...DEFAULT_SERVER_SETTINGS, ...DEFAULT_CLIENT_SETTINGS, }; // ── Server Settings Patch (replace with a Schema.deepPartial if available) ────────────────────────────────────────── const ModelSelectionPatch = Schema.Struct({ instanceId: Schema.optionalKey(ProviderInstanceId), model: Schema.optionalKey(TrimmedNonEmptyString), options: Schema.optionalKey(ProviderOptionSelections), }); const CodexSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), homePath: Schema.optionalKey(TrimmedString), shadowHomePath: Schema.optionalKey(TrimmedString), launchArgs: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), }); const ClaudeSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), homePath: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), launchArgs: Schema.optionalKey(TrimmedString), // Validated at the patch boundary so a typo fails the one update with a // schema error instead of a generic whole-settings failure. autoCompactWindow: Schema.optionalKey( TrimmedString.check(Schema.isPattern(CLAUDE_AUTO_COMPACT_WINDOW_PATTERN)), ), }); const CursorSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), apiEndpoint: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), }); const GrokSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), }); const AntigravitySettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), authMethod: Schema.optionalKey(AntigravityAuthMethod), apiKey: Schema.optionalKey(TrimmedString), gcpProject: Schema.optionalKey(TrimmedString), gcpLocation: Schema.optionalKey(TrimmedString), binaryPath: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), }); const OpenCodeSettingsPatch = Schema.Struct({ enabled: Schema.optionalKey(Schema.Boolean), binaryPath: Schema.optionalKey(TrimmedString), serverUrl: Schema.optionalKey(TrimmedString), serverPassword: Schema.optionalKey(TrimmedString), customModels: Schema.optionalKey(Schema.Array(CustomModelSetting)), }); export const ServerSettingsPatch = Schema.Struct({ // Server settings enableLegacyTokenStreaming: Schema.optionalKey(Schema.Boolean), enableProviderUpdateChecks: Schema.optionalKey(Schema.Boolean), continueThreadsAfterServerUpdate: Schema.optionalKey(Schema.Boolean), enableAgentBrowserAccess: Schema.optionalKey(Schema.Boolean), projectAgentBrowserAccessOverrides: Schema.optionalKey( Schema.Record(ProjectId, Schema.NullOr(Schema.Boolean)), ), defaultAutoPull: Schema.optionalKey(Schema.Boolean), defaultProjectScripts: Schema.optionalKey(Schema.Array(ProjectScript)), projectScriptOverrides: Schema.optionalKey( Schema.Record(ProjectId, Schema.NullOr(Schema.Array(ProjectScript))), ), projectAutoPullOverrides: Schema.optionalKey( Schema.Record(ProjectId, Schema.NullOr(Schema.Boolean)), ), defaultModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), enableAgentDeviceAccess: Schema.optionalKey(Schema.Boolean), enableDeviceSupport: Schema.optionalKey(Schema.Boolean), deviceOnboardingCompleted: Schema.optionalKey(Schema.Boolean), deviceHosts: Schema.optionalKey(SshDeviceHostConfigs), sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)), sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean), backgroundActivity: Schema.optionalKey( Schema.Struct({ schemaVersion: Schema.optionalKey(Schema.Literal(1)), profile: Schema.optionalKey(BackgroundActivityProfileSelection), baseProfile: Schema.optionalKey(BackgroundActivityProfile), overrides: Schema.optionalKey(BackgroundActivityOverrides), }), ), automaticGitFetchInterval: Schema.optionalKey(Schema.DurationFromMillis), providerHealthRefreshInterval: Schema.optionalKey(Schema.DurationFromMillis), backgroundActivityProfile: Schema.optionalKey(BackgroundActivityProfile), environmentIcon: Schema.optionalKey(Schema.NullOr(EnvironmentMachineKind)), defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode), newWorktreesStartFromOrigin: Schema.optionalKey(Schema.Boolean), addProjectBaseDirectory: Schema.optionalKey(TrimmedString), textGenerationModelSelection: Schema.optionalKey(ModelSelectionPatch), sourceControlWritingStyle: Schema.optionalKey( Schema.Struct({ mode: Schema.optionalKey(SourceControlWritingStyleMode), customInstructions: Schema.optionalKey(TrimmedString), followChangeRequestTemplates: Schema.optionalKey(Schema.Boolean), }), ), sourceControlWriterModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), observability: Schema.optionalKey( Schema.Struct({ otlpTracesUrl: Schema.optionalKey(TrimmedString), otlpMetricsUrl: Schema.optionalKey(TrimmedString), }), ), providers: Schema.optionalKey( Schema.Struct({ codex: Schema.optionalKey(CodexSettingsPatch), claudeAgent: Schema.optionalKey(ClaudeSettingsPatch), cursor: Schema.optionalKey(CursorSettingsPatch), grok: Schema.optionalKey(GrokSettingsPatch), opencode: Schema.optionalKey(OpenCodeSettingsPatch), antigravity: Schema.optionalKey(AntigravitySettingsPatch), }), ), // Whole-map replacement for the new instance config. Patching individual // entries is intentionally out of scope: the map is small, and partial // patches risk leaving driver-specific config in a half-merged state. // The web UI sends a fully-formed map every time it edits this field. providerInstances: Schema.optionalKey(Schema.Record(ProviderInstanceId, ProviderInstanceConfig)), // Per-entry, unlike `providerInstances`: a client only ever adds or removes // one source, and sending the whole map races another edit that has not // echoed back yet. `null` removes; the server merges into its current map. usageLimitSources: Schema.optionalKey( Schema.Record(UsageLimitSourceId, Schema.NullOr(UsageLimitSourceConfig)), ), /** Each entry replaces one model's rates; `null` restores automatic pricing. */ usagePriceOverrides: Schema.optionalKey( Schema.Record(TrimmedNonEmptyString, Schema.NullOr(UsageModelPriceOverride)), ), }); export type ServerSettingsPatch = typeof ServerSettingsPatch.Type; export const ClientSettingsPatch = Schema.Struct({ diffColorScheme: Schema.optionalKey(DiffColorScheme), loadBalancingEnabled: Schema.optionalKey(Schema.Boolean), loadBalancingWeights: Schema.optionalKey(LoadBalancingWeights), appearanceContrast: Schema.optionalKey(AppearanceContrast), panelAnimationDurationMs: Schema.optionalKey(PanelAnimationDurationMs), browserDefaultViewport: Schema.optionalKey(PreviewViewportSetting), browserDefaultZoomFactor: Schema.optionalKey(PreviewZoomFactor), browserDefaultAppearance: Schema.optionalKey(PreviewAppearancePreference), browserRecordingFrameRate: Schema.optionalKey(BrowserRecordingFrameRate), browserLinkTarget: Schema.optionalKey(BrowserLinkTarget), browserAutoShowFloatingPreview: Schema.optionalKey(Schema.Boolean), browserProfiles: Schema.optionalKey(Schema.Array(BrowserProfile)), browserDefaultProfileId: Schema.optionalKey(BrowserProfileId), confirmQuit: Schema.optionalKey(QuitConfirmationMode), confirmThreadArchive: Schema.optionalKey(Schema.Boolean), confirmThreadDelete: Schema.optionalKey(Schema.Boolean), confirmThreadUnpin: Schema.optionalKey(Schema.Boolean), diffIgnoreWhitespace: Schema.optionalKey(Schema.Boolean), diffLayout: Schema.optionalKey(DiffLayout), environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), glassOpacity: Schema.optionalKey(GlassOpacity), onboardingCompletedAt: Schema.optionalKey(Schema.NullOr(Schema.String)), fontSizeInterface: Schema.optionalKey(InterfaceFontSize), fontSizePrompt: Schema.optionalKey(PromptFontSize), fontSizeCode: Schema.optionalKey(CodeFontSize), fontSizeTerminal: Schema.optionalKey(TerminalFontSize), fontFamilyCode: Schema.optionalKey(FontFamilyPreference), fontFamilyComposer: Schema.optionalKey(FontFamilyPreference), fontFamilySans: Schema.optionalKey(FontFamilyPreference), fontFamilyTerminal: Schema.optionalKey(FontFamilyPreference), fontSmoothing: Schema.optionalKey(Schema.Boolean), favorites: Schema.optionalKey( Schema.Array( Schema.Struct({ provider: ProviderInstanceId, model: TrimmedNonEmptyString, }), ), ), providerModelPreferences: Schema.optionalKey( 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([])), ), }), ), ), pullRequestMergeMethodOverrides: Schema.optionalKey( Schema.Record(TrimmedNonEmptyString, PullRequestMergeMethod), ), planModeEnabled: Schema.optionalKey(Schema.Boolean), contextWindowMeterEnabled: Schema.optionalKey(Schema.Boolean), composerCollapseOnScroll: Schema.optionalKey(Schema.Boolean), proactivePanelsEnabled: Schema.optionalKey(Schema.Boolean), showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), sidebarProjectGroupingOverrides: Schema.optionalKey( Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), ), sidebarProjectSortOrder: Schema.optionalKey(SidebarProjectSortOrder), sidebarThreadSortOrder: Schema.optionalKey(SidebarThreadSortOrder), sidebarThreadPreviewCount: Schema.optionalKey(SidebarThreadPreviewCount), timestampFormat: Schema.optionalKey(TimestampFormat), snapShotEnabled: Schema.optionalKey(Schema.Boolean), snapShotIncludeAccessibility: Schema.optionalKey(Schema.Boolean), snapShotShortcut: Schema.optionalKey(SnapShotShortcut), snapShotPlaySound: Schema.optionalKey(Schema.Boolean), snapShotSound: Schema.optionalKey(SnapShotSound), snapShotFlash: Schema.optionalKey(Schema.Boolean), snapShotAnimations: Schema.optionalKey(Schema.Boolean), wordWrap: Schema.optionalKey(Schema.Boolean), }); export type ClientSettingsPatch = typeof ClientSettingsPatch.Type;