History

mv to contract package

Julius Marminge committed 7 months ago

Commit 619fd201 parent c91bd2612 files · +216 −165Browse files

Unified · Split

12 files
  1. apps/web/src/clientSettings.ts +0 −50
  2. apps/web/src/components/ChatView.browser.tsx +1 −1
  3. apps/web/src/components/PlanSidebar.tsx +1 −1
  4. apps/web/src/components/Sidebar.logic.ts +1 −1
  5. apps/web/src/components/Sidebar.tsx +4 −1
  6. apps/web/src/components/chat/MessagesTimeline.tsx +1 −1
  7. apps/web/src/components/chat/TraitsPicker.browser.tsx +1 −1
  8. apps/web/src/hooks/useSettings.ts +55 −15
  9. apps/web/src/timestampFormat.ts +1 −1
  10. packages/contracts/package.json +5 −0
  11. packages/contracts/src/server.ts +17 −93
  12. packages/contracts/src/settings.ts +129 −0
apps/web/src/clientSettings.ts+0−50
@@ -1,50 +0,0 @@
1/**
2 * Client-only settings - persisted in localStorage.
3 *
4 * These settings affect only the frontend UX and never need to be read by
5 * the server. Server-authoritative settings live in `ServerSettings`
6 * (defined in `@t3tools/contracts/server`).
7 */
8import { Option, Schema } from "effect";
9
10export const CLIENT_SETTINGS_STORAGE_KEY = "t3code:client-settings:v1";
11
12export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
13export type TimestampFormat = typeof TimestampFormat.Type;
14export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
15
16export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);
17export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;
18export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at";
19
20export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at"]);
21export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
22export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";
23
24const withDefaults =
25 <
26 S extends Schema.Top & Schema.WithoutConstructorDefault,
27 D extends S["~type.make.in"] & S["Encoded"],
28 >(
29 fallback: () => D,
30 ) =>
31 (schema: S) =>
32 schema.pipe(
33 Schema.withConstructorDefault(() => Option.some(fallback())),
34 Schema.withDecodingDefault(() => fallback()),
35 );
36
37export const ClientSettingsSchema = Schema.Struct({
38 confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
39 diffWordWrap: Schema.Boolean.pipe(withDefaults(() => false)),
40 sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
41 withDefaults(() => DEFAULT_SIDEBAR_PROJECT_SORT_ORDER),
42 ),
43 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(
44 withDefaults(() => DEFAULT_SIDEBAR_THREAD_SORT_ORDER),
45 ),
46 timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)),
47});
48export type ClientSettings = typeof ClientSettingsSchema.Type;
49
50export const DEFAULT_CLIENT_SETTINGS: ClientSettings = ClientSettingsSchema.makeUnsafe({});
apps/web/src/components/ChatView.browser.tsx+1−1
@@ -31,7 +31,7 @@ import { isMacPlatform } from "../lib/utils"; · 30 unmodified lines
31import { getRouter } from "../router";31import { getRouter } from "../router";
32import { useStore } from "../store";32import { useStore } from "../store";
33import { estimateTimelineMessageHeight } from "./timelineHeight";33import { estimateTimelineMessageHeight } from "./timelineHeight";
34import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings";34import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
35 35
36const THREAD_ID = "thread-browser-test" as ThreadId;36const THREAD_ID = "thread-browser-test" as ThreadId;
37const UUID_ROUTE_RE = /^\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;37const UUID_ROUTE_RE = /^\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
apps/web/src/components/PlanSidebar.tsx+1−1
@@ -1,5 +1,5 @@
1import { memo, useState, useCallback } from "react";1import { memo, useState, useCallback } from "react";
2import { type TimestampFormat } from "../clientSettings";2import { type TimestampFormat } from "@t3tools/contracts/settings";
3import { Badge } from "./ui/badge";3import { Badge } from "./ui/badge";
4import { Button } from "./ui/button";4import { Button } from "./ui/button";
5import { ScrollArea } from "./ui/scroll-area";5import { ScrollArea } from "./ui/scroll-area";
apps/web/src/components/Sidebar.logic.ts+1−1
@@ -1,4 +1,4 @@
1import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "../clientSettings";1import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
2import type { Thread } from "../types";2import type { Thread } from "../types";
3import { cn } from "../lib/utils";3import { cn } from "../lib/utils";
4import {4import {
apps/web/src/components/Sidebar.tsx+4−1
@@ -38,7 +38,10 @@ import { · 37 unmodified lines
38} from "@t3tools/contracts";38} from "@t3tools/contracts";
39import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";39import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
40import { useLocation, useNavigate, useParams } from "@tanstack/react-router";40import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
41import { type SidebarProjectSortOrder, type SidebarThreadSortOrder } from "../clientSettings";41import {
42 type SidebarProjectSortOrder,
43 type SidebarThreadSortOrder,
44} from "@t3tools/contracts/settings";
42import { isElectron } from "../env";45import { isElectron } from "../env";
43import { APP_STAGE_LABEL, APP_VERSION } from "../branding";46import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
44import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";47import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
apps/web/src/components/chat/MessagesTimeline.tsx+1−1
@@ -48,7 +48,7 @@ import { · 47 unmodified lines
48 type ParsedTerminalContextEntry,48 type ParsedTerminalContextEntry,
49} from "~/lib/terminalContext";49} from "~/lib/terminalContext";
50import { cn } from "~/lib/utils";50import { cn } from "~/lib/utils";
51import { type TimestampFormat } from "../../clientSettings";51import { type TimestampFormat } from "@t3tools/contracts/settings";
52import { formatTimestamp } from "../../timestampFormat";52import { formatTimestamp } from "../../timestampFormat";
53import {53import {
54 buildInlineTerminalContextText,54 buildInlineTerminalContextText,
apps/web/src/components/chat/TraitsPicker.browser.tsx+1−1
@@ -22,7 +22,7 @@ import { · 21 unmodified lines
22 useComposerThreadDraft,22 useComposerThreadDraft,
23 useEffectiveComposerModelState,23 useEffectiveComposerModelState,
24} from "../../composerDraftStore";24} from "../../composerDraftStore";
25import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings";25import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
26 26
27// ── Claude TraitsPicker tests ─────────────────────────────────────────27// ── Claude TraitsPicker tests ─────────────────────────────────────────
28 28
apps/web/src/hooks/useSettings.ts+55−15
@@ -19,16 +19,19 @@ import { · 18 unmodified lines
19 ThreadEnvMode,19 ThreadEnvMode,
20} from "@t3tools/contracts";20} from "@t3tools/contracts";
21import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts";21import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts";
22import {
23 type ClientSettings,
24 ClientSettingsSchema,
25 CLIENT_SETTINGS_STORAGE_KEY,
26 DEFAULT_CLIENT_SETTINGS,
27 SidebarProjectSortOrder,
28 SidebarThreadSortOrder,
29 TimestampFormat,
30} from "@t3tools/contracts/settings";
22import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";31import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
23import { ensureNativeApi } from "~/nativeApi";32import { ensureNativeApi } from "~/nativeApi";
24import { useLocalStorage } from "./useLocalStorage";33import { useLocalStorage } from "./useLocalStorage";
25import { normalizeCustomModelSlugs } from "~/modelSelection";34import { normalizeCustomModelSlugs } from "~/modelSelection";
26import {
27 ClientSettingsSchema,
28 type ClientSettings,
29 DEFAULT_CLIENT_SETTINGS,
30 CLIENT_SETTINGS_STORAGE_KEY,
31} from "~/clientSettings";
32import { Predicate, Schema, Struct } from "effect";35import { Predicate, Schema, Struct } from "effect";
33import { DeepMutable } from "effect/Types";36import { DeepMutable } from "effect/Types";
34import { deepMerge } from "@t3tools/shared/Struct";37import { deepMerge } from "@t3tools/shared/Struct";
@@ -199,9 +202,38 @@ export function buildLegacyServerSettingsMigrationPatch(legacySettings: Record<s · 164 unmodified lines
199 return patch;202 return patch;
200}203}
201 204
205export function buildLegacyClientSettingsMigrationPatch(
206 legacySettings: Record<string, unknown>,
207): Partial<DeepMutable<ClientSettings>> {
208 const patch: Partial<DeepMutable<ClientSettings>> = {};
209
210 if (Predicate.isBoolean(legacySettings.confirmThreadDelete)) {
211 patch.confirmThreadDelete = legacySettings.confirmThreadDelete;
212 }
213
214 if (Predicate.isBoolean(legacySettings.diffWordWrap)) {
215 patch.diffWordWrap = legacySettings.diffWordWrap;
216 }
217
218 if (Schema.is(SidebarProjectSortOrder)(legacySettings.sidebarProjectSortOrder)) {
219 patch.sidebarProjectSortOrder = legacySettings.sidebarProjectSortOrder;
220 }
221
222 if (Schema.is(SidebarThreadSortOrder)(legacySettings.sidebarThreadSortOrder)) {
223 patch.sidebarThreadSortOrder = legacySettings.sidebarThreadSortOrder;
224 }
225
226 if (Schema.is(TimestampFormat)(legacySettings.timestampFormat)) {
227 patch.timestampFormat = legacySettings.timestampFormat;
228 }
229
230 return patch;
231}
232
202/**233/**
203 * Call once on app startup. Migrates server-relevant settings from the234 * Call once on app startup. Migrates settings from the old unified
204 * old localStorage key to the server. Idempotent.235 * localStorage key to the server (server-relevant keys) and to the new
236 * client-settings localStorage key (client-only keys). Idempotent.
205 */237 */
206export function migrateLocalSettingsToServer(): void {238export function migrateLocalSettingsToServer(): void {
207 if (typeof window === "undefined") return;239 if (typeof window === "undefined") return;
@@ -217,19 +249,27 @@ export function migrateLocalSettingsToServer(): void { · 9 unmodified lines
217 const old = JSON.parse(raw);249 const old = JSON.parse(raw);
218 if (!Predicate.isObject(old)) return;250 if (!Predicate.isObject(old)) return;
219 251
220 const api = ensureNativeApi();252 // Migrate server-relevant keys via RPC
221
222 const serverPatch = buildLegacyServerSettingsMigrationPatch(old);253 const serverPatch = buildLegacyServerSettingsMigrationPatch(old);
254 if (Object.keys(serverPatch).length > 0) {
255 const api = ensureNativeApi();
256 void api.server.updateSettings(serverPatch);
257 }
223 258
224 if (Object.keys(serverPatch).length === 0) {259 // Migrate client-only keys to the new localStorage key
225 localStorage.setItem(MIGRATION_FLAG_KEY, "true");260 const clientPatch = buildLegacyClientSettingsMigrationPatch(old);
226 return;261 if (Object.keys(clientPatch).length > 0) {
262 const existing = localStorage.getItem(CLIENT_SETTINGS_STORAGE_KEY);
263 const current = existing ? (JSON.parse(existing) as Record<string, unknown>) : {};
264 localStorage.setItem(
265 CLIENT_SETTINGS_STORAGE_KEY,
266 JSON.stringify({ ...current, ...clientPatch }),
267 );
227 }268 }
228 269
229 void api.server.updateSettings(serverPatch);
230 localStorage.setItem(MIGRATION_FLAG_KEY, "true");270 localStorage.setItem(MIGRATION_FLAG_KEY, "true");
231 } catch (error) {271 } catch (error) {
232 console.error("[MIGRATION] Error migrating local settings to server:", error);272 console.error("[MIGRATION] Error migrating local settings:", error);
233 } finally {273 } finally {
234 // Mark as migrated no matter what to avoid retrying274 // Mark as migrated no matter what to avoid retrying
235 localStorage.setItem(MIGRATION_FLAG_KEY, "true");275 localStorage.setItem(MIGRATION_FLAG_KEY, "true");
apps/web/src/timestampFormat.ts+1−1
@@ -1,4 +1,4 @@
1import { type TimestampFormat } from "./clientSettings";1import { type TimestampFormat } from "@t3tools/contracts/settings";
2 2
3export function getTimestampFormatOptions(3export function getTimestampFormatOptions(
4 timestampFormat: TimestampFormat,4 timestampFormat: TimestampFormat,
packages/contracts/package.json+5−0
@@ -10,6 +10,11 @@ · 9 unmodified lines
10 "module": "./dist/index.mjs",10 "module": "./dist/index.mjs",
11 "types": "./src/index.ts",11 "types": "./src/index.ts",
12 "exports": {12 "exports": {
13 "./settings": {
14 "types": "./src/settings.ts",
15 "import": "./src/settings.ts",
16 "require": "./src/settings.ts"
17 },
13 ".": {18 ".": {
14 "types": "./src/index.ts",19 "types": "./src/index.ts",
15 "import": "./src/index.ts",20 "import": "./src/index.ts",
packages/contracts/src/server.ts+17−93
@@ -2,100 +2,24 @@ import { Schema } from "effect"; · 1 unmodified line
2import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas";2import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas";
3import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings";3import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings";
4import { EditorId } from "./editor";4import { EditorId } from "./editor";
5import { ProviderKind } from "./orchestration";
5import {6import {
6 ClaudeModelOptions,7 DEFAULT_SERVER_SETTINGS,
7 CodexModelOptions,8 CodexSettings,
8 DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,9 ClaudeSettings,
9} from "./model";10 ServerSettings,
10import { ModelSelection, ProviderKind } from "./orchestration";11 ServerSettingsPatch,
11 12 ThreadEnvMode,
12// ── Server Settings (server-authoritative) ───────────────────────────13} from "./settings";
13 14
14export const ThreadEnvMode = Schema.Literals(["local", "worktree"]);15export {
15export type ThreadEnvMode = typeof ThreadEnvMode.Type;16 DEFAULT_SERVER_SETTINGS,
16 17 ServerSettings,
17export const CodexSettings = Schema.Struct({18 ServerSettingsPatch,
18 binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),19 ThreadEnvMode,
19 homePath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),20 CodexSettings,
20 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),21 ClaudeSettings,
21});22};
22export type CodexSettings = typeof CodexSettings.Type;
23
24export const ClaudeSettings = Schema.Struct({
25 binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
26 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
27});
28export type ClaudeSettings = typeof ClaudeSettings.Type;
29
30export const ServerSettings = Schema.Struct({
31 enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
32 defaultThreadEnvMode: ThreadEnvMode.pipe(
33 Schema.withDecodingDefault(() => "local" as const satisfies ThreadEnvMode),
34 ),
35 textGenerationModelSelection: ModelSelection.pipe(
36 Schema.withDecodingDefault(() => ({
37 provider: "codex" as const,
38 model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex,
39 })),
40 ),
41
42 // Provider specific settings
43 providers: Schema.Struct({
44 codex: CodexSettings.pipe(Schema.withDecodingDefault(() => ({}))),
45 claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(() => ({}))),
46 }).pipe(Schema.withDecodingDefault(() => ({}))),
47});
48export type ServerSettings = typeof ServerSettings.Type;
49
50export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeUnknownSync(ServerSettings)({});
51
52const CodexModelOptionsPatch = Schema.Struct({
53 reasoningEffort: Schema.optionalKey(CodexModelOptions.fields.reasoningEffort),
54 fastMode: Schema.optionalKey(CodexModelOptions.fields.fastMode),
55});
56
57const ClaudeModelOptionsPatch = Schema.Struct({
58 thinking: Schema.optionalKey(ClaudeModelOptions.fields.thinking),
59 effort: Schema.optionalKey(ClaudeModelOptions.fields.effort),
60 fastMode: Schema.optionalKey(ClaudeModelOptions.fields.fastMode),
61});
62
63const ModelSelectionPatch = Schema.Union([
64 Schema.Struct({
65 provider: Schema.optionalKey(Schema.Literal("codex")),
66 model: Schema.optionalKey(TrimmedNonEmptyString),
67 options: Schema.optionalKey(CodexModelOptionsPatch),
68 }),
69 Schema.Struct({
70 provider: Schema.optionalKey(Schema.Literal("claudeAgent")),
71 model: Schema.optionalKey(TrimmedNonEmptyString),
72 options: Schema.optionalKey(ClaudeModelOptionsPatch),
73 }),
74]);
75
76const CodexSettingsPatch = Schema.Struct({
77 binaryPath: Schema.optionalKey(Schema.String),
78 homePath: Schema.optionalKey(Schema.String),
79 customModels: Schema.optionalKey(Schema.Array(Schema.String)),
80});
81
82const ClaudeSettingsPatch = Schema.Struct({
83 binaryPath: Schema.optionalKey(Schema.String),
84 customModels: Schema.optionalKey(Schema.Array(Schema.String)),
85});
86
87export const ServerSettingsPatch = Schema.Struct({
88 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
89 defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
90 textGenerationModelSelection: Schema.optionalKey(ModelSelectionPatch),
91 providers: Schema.optionalKey(
92 Schema.Struct({
93 codex: Schema.optionalKey(CodexSettingsPatch),
94 claudeAgent: Schema.optionalKey(ClaudeSettingsPatch),
95 }),
96 ),
97});
98export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
99 23
100const KeybindingsMalformedConfigIssue = Schema.Struct({24const KeybindingsMalformedConfigIssue = Schema.Struct({
101 kind: Schema.Literal("keybindings.malformed-config"),25 kind: Schema.Literal("keybindings.malformed-config"),
packages/contracts/src/settings.ts+129−0
@@ -0,0 +1,129 @@
1import * as Schema from "effect/Schema";
2import { TrimmedNonEmptyString } from "./baseSchemas";
3import {
4 ClaudeModelOptions,
5 CodexModelOptions,
6 DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
7} from "./model";
8import { ModelSelection } from "./orchestration";
9
10// ── Client Settings (local-only) ───────────────────────────────
11
12export const CLIENT_SETTINGS_STORAGE_KEY = "t3code:client-settings:v1";
13
14export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
15export type TimestampFormat = typeof TimestampFormat.Type;
16export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
17
18export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);
19export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;
20export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at";
21
22export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at"]);
23export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
24export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";
25
26export const ClientSettingsSchema = Schema.Struct({
27 confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(() => true)),
28 diffWordWrap: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
29 sidebarProjectSortOrder: SidebarProjectSortOrder.pipe(
30 Schema.withDecodingDefault(() => DEFAULT_SIDEBAR_PROJECT_SORT_ORDER),
31 ),
32 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(
33 Schema.withDecodingDefault(() => DEFAULT_SIDEBAR_THREAD_SORT_ORDER),
34 ),
35 timestampFormat: TimestampFormat.pipe(Schema.withDecodingDefault(() => DEFAULT_TIMESTAMP_FORMAT)),
36});
37export type ClientSettings = typeof ClientSettingsSchema.Type;
38
39export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientSettingsSchema)({});
40
41// ── Server Settings (server-authoritative) ────────────────────
42
43export const ThreadEnvMode = Schema.Literals(["local", "worktree"]);
44export type ThreadEnvMode = typeof ThreadEnvMode.Type;
45
46export const CodexSettings = Schema.Struct({
47 binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
48 homePath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
49 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
50});
51export type CodexSettings = typeof CodexSettings.Type;
52
53export const ClaudeSettings = Schema.Struct({
54 binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
55 customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
56});
57export type ClaudeSettings = typeof ClaudeSettings.Type;
58
59export const ServerSettings = Schema.Struct({
60 enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(() => false)),
61 defaultThreadEnvMode: ThreadEnvMode.pipe(
62 Schema.withDecodingDefault(() => "local" as const satisfies ThreadEnvMode),
63 ),
64 textGenerationModelSelection: ModelSelection.pipe(
65 Schema.withDecodingDefault(() => ({
66 provider: "codex" as const,
67 model: DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER.codex,
68 })),
69 ),
70
71 // Provider specific settings
72 providers: Schema.Struct({
73 codex: CodexSettings.pipe(Schema.withDecodingDefault(() => ({}))),
74 claudeAgent: ClaudeSettings.pipe(Schema.withDecodingDefault(() => ({}))),
75 }).pipe(Schema.withDecodingDefault(() => ({}))),
76});
77export type ServerSettings = typeof ServerSettings.Type;
78
79export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({});
80
81// ── Server Settings Patch (replace with a Schema.deepPartial if available) ──────────────────────────────────────────
82
83const CodexModelOptionsPatch = Schema.Struct({
84 reasoningEffort: Schema.optionalKey(CodexModelOptions.fields.reasoningEffort),
85 fastMode: Schema.optionalKey(CodexModelOptions.fields.fastMode),
86});
87
88const ClaudeModelOptionsPatch = Schema.Struct({
89 thinking: Schema.optionalKey(ClaudeModelOptions.fields.thinking),
90 effort: Schema.optionalKey(ClaudeModelOptions.fields.effort),
91 fastMode: Schema.optionalKey(ClaudeModelOptions.fields.fastMode),
92});
93
94const ModelSelectionPatch = Schema.Union([
95 Schema.Struct({
96 provider: Schema.optionalKey(Schema.Literal("codex")),
97 model: Schema.optionalKey(TrimmedNonEmptyString),
98 options: Schema.optionalKey(CodexModelOptionsPatch),
99 }),
100 Schema.Struct({
101 provider: Schema.optionalKey(Schema.Literal("claudeAgent")),
102 model: Schema.optionalKey(TrimmedNonEmptyString),
103 options: Schema.optionalKey(ClaudeModelOptionsPatch),
104 }),
105]);
106
107const CodexSettingsPatch = Schema.Struct({
108 binaryPath: Schema.optionalKey(Schema.String),
109 homePath: Schema.optionalKey(Schema.String),
110 customModels: Schema.optionalKey(Schema.Array(Schema.String)),
111});
112
113const ClaudeSettingsPatch = Schema.Struct({
114 binaryPath: Schema.optionalKey(Schema.String),
115 customModels: Schema.optionalKey(Schema.Array(Schema.String)),
116});
117
118export const ServerSettingsPatch = Schema.Struct({
119 enableAssistantStreaming: Schema.optionalKey(Schema.Boolean),
120 defaultThreadEnvMode: Schema.optionalKey(ThreadEnvMode),
121 textGenerationModelSelection: Schema.optionalKey(ModelSelectionPatch),
122 providers: Schema.optionalKey(
123 Schema.Struct({
124 codex: Schema.optionalKey(CodexSettingsPatch),
125 claudeAgent: Schema.optionalKey(ClaudeSettingsPatch),
126 }),
127 ),
128});
129export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;