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, type ProviderDriverKind,} 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( { // Off by default (like Grok 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("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( { // 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(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath"], },);export type GrokSettings = typeof GrokSettings.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(Lines 1–500 of 918. Load more
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, type ProviderDriverKind, } 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( { // Off by default (like Grok 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("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( { // 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(Schema.String).pipe( Schema.withDecodingDefault(Effect.succeed([])), Schema.annotateKey({ providerSettingsForm: { hidden: true } }), ), }, { order: ["binaryPath"], }, ); export type GrokSettings = typeof GrokSettings.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(