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− */
8−import { Option, Schema } from "effect";
9−
10−export const CLIENT_SETTINGS_STORAGE_KEY = "t3code:client-settings:v1";
11−
12−export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
13−export type TimestampFormat = typeof TimestampFormat.Type;
14−export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
15−
16−export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);
17−export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;
18−export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at";
19−
20−export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at"]);
21−export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
22−export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";
23−
24−const 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−
37−export 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−});
48−export type ClientSettings = typeof ClientSettingsSchema.Type;
49−
50−export 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
3131 import { getRouter } from "../router";
3232 import { useStore } from "../store";
3333 import { estimateTimelineMessageHeight } from "./timelineHeight";
34−import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings";
34+import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
3535
3636 const THREAD_ID = "thread-browser-test" as ThreadId;
3737 const 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 @@
11 import { memo, useState, useCallback } from "react";
2−import { type TimestampFormat } from "../clientSettings";
2+import { type TimestampFormat } from "@t3tools/contracts/settings";
33 import { Badge } from "./ui/badge";
44 import { Button } from "./ui/button";
55 import { ScrollArea } from "./ui/scroll-area";
apps/web/src/components/Sidebar.logic.ts+1−1
@@ -1,4 +1,4 @@
1−import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "../clientSettings";
1+import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
22 import type { Thread } from "../types";
33 import { cn } from "../lib/utils";
44 import {
apps/web/src/components/Sidebar.tsx+4−1
@@ -38,7 +38,10 @@ import { · 37 unmodified lines
3838 } from "@t3tools/contracts";
3939 import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
4040 import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
41−import { type SidebarProjectSortOrder, type SidebarThreadSortOrder } from "../clientSettings";
41+import {
42+ type SidebarProjectSortOrder,
43+ type SidebarThreadSortOrder,
44+} from "@t3tools/contracts/settings";
4245 import { isElectron } from "../env";
4346 import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
4447 import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils";
apps/web/src/components/chat/MessagesTimeline.tsx+1−1
@@ -48,7 +48,7 @@ import { · 47 unmodified lines
4848 type ParsedTerminalContextEntry,
4949 } from "~/lib/terminalContext";
5050 import { cn } from "~/lib/utils";
51−import { type TimestampFormat } from "../../clientSettings";
51+import { type TimestampFormat } from "@t3tools/contracts/settings";
5252 import { formatTimestamp } from "../../timestampFormat";
5353 import {
5454 buildInlineTerminalContextText,
apps/web/src/components/chat/TraitsPicker.browser.tsx+1−1
@@ -22,7 +22,7 @@ import { · 21 unmodified lines
2222 useComposerThreadDraft,
2323 useEffectiveComposerModelState,
2424 } from "../../composerDraftStore";
25−import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings";
25+import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
2626
2727 // ── Claude TraitsPicker tests ─────────────────────────────────────────
2828
apps/web/src/hooks/useSettings.ts+55−15
@@ -19,16 +19,19 @@ import { · 18 unmodified lines
1919 ThreadEnvMode,
2020 } from "@t3tools/contracts";
2121 import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts";
22+import {
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";
2231 import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery";
2332 import { ensureNativeApi } from "~/nativeApi";
2433 import { useLocalStorage } from "./useLocalStorage";
2534 import { normalizeCustomModelSlugs } from "~/modelSelection";
26−import {
27− ClientSettingsSchema,
28− type ClientSettings,
29− DEFAULT_CLIENT_SETTINGS,
30− CLIENT_SETTINGS_STORAGE_KEY,
31−} from "~/clientSettings";
3235 import { Predicate, Schema, Struct } from "effect";
3336 import { DeepMutable } from "effect/Types";
3437 import { deepMerge } from "@t3tools/shared/Struct";
@@ -199,9 +202,38 @@ export function buildLegacyServerSettingsMigrationPatch(legacySettings: Record<s · 164 unmodified lines
199202 return patch;
200203 }
201204
205+export 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+
202233 /**
203− * Call once on app startup. Migrates server-relevant settings from the
204− * old localStorage key to the server. Idempotent.
234+ * Call once on app startup. Migrates settings from the old unified
235+ * localStorage key to the server (server-relevant keys) and to the new
236+ * client-settings localStorage key (client-only keys). Idempotent.
205237 */
206238 export function migrateLocalSettingsToServer(): void {
207239 if (typeof window === "undefined") return;
@@ -217,19 +249,27 @@ export function migrateLocalSettingsToServer(): void { · 9 unmodified lines
217249 const old = JSON.parse(raw);
218250 if (!Predicate.isObject(old)) return;
219251
220− const api = ensureNativeApi();
221−
252+ // Migrate server-relevant keys via RPC
222253 const serverPatch = buildLegacyServerSettingsMigrationPatch(old);
254+ if (Object.keys(serverPatch).length > 0) {
255+ const api = ensureNativeApi();
256+ void api.server.updateSettings(serverPatch);
257+ }
223258
224− if (Object.keys(serverPatch).length === 0) {
225− localStorage.setItem(MIGRATION_FLAG_KEY, "true");
226− return;
259+ // Migrate client-only keys to the new localStorage key
260+ const clientPatch = buildLegacyClientSettingsMigrationPatch(old);
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+ );
227268 }
228269
229− void api.server.updateSettings(serverPatch);
230270 localStorage.setItem(MIGRATION_FLAG_KEY, "true");
231271 } catch (error) {
232− console.error("[MIGRATION] Error migrating local settings to server:", error);
272+ console.error("[MIGRATION] Error migrating local settings:", error);
233273 } finally {
234274 // Mark as migrated no matter what to avoid retrying
235275 localStorage.setItem(MIGRATION_FLAG_KEY, "true");
apps/web/src/timestampFormat.ts+1−1
@@ -1,4 +1,4 @@
1−import { type TimestampFormat } from "./clientSettings";
1+import { type TimestampFormat } from "@t3tools/contracts/settings";
22
33 export function getTimestampFormatOptions(
44 timestampFormat: TimestampFormat,
packages/contracts/package.json+5−0
@@ -10,6 +10,11 @@ · 9 unmodified lines
1010 "module": "./dist/index.mjs",
1111 "types": "./src/index.ts",
1212 "exports": {
13+ "./settings": {
14+ "types": "./src/settings.ts",
15+ "import": "./src/settings.ts",
16+ "require": "./src/settings.ts"
17+ },
1318 ".": {
1419 "types": "./src/index.ts",
1520 "import": "./src/index.ts",
packages/contracts/src/server.ts+17−93
@@ -2,100 +2,24 @@ import { Schema } from "effect"; · 1 unmodified line
22 import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas";
33 import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings";
44 import { EditorId } from "./editor";
5+import { ProviderKind } from "./orchestration";
56 import {
6− ClaudeModelOptions,
7− CodexModelOptions,
8− DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
9−} from "./model";
10−import { ModelSelection, ProviderKind } from "./orchestration";
11−
12−// ── Server Settings (server-authoritative) ───────────────────────────
13−
14−export const ThreadEnvMode = Schema.Literals(["local", "worktree"]);
15−export type ThreadEnvMode = typeof ThreadEnvMode.Type;
16−
17−export const CodexSettings = Schema.Struct({
18− binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
19− homePath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
20− customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
21−});
22−export type CodexSettings = typeof CodexSettings.Type;
23−
24−export const ClaudeSettings = Schema.Struct({
25− binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
26− customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
27−});
28−export type ClaudeSettings = typeof ClaudeSettings.Type;
29−
30−export 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−});
48−export type ServerSettings = typeof ServerSettings.Type;
49−
50−export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeUnknownSync(ServerSettings)({});
51−
52−const CodexModelOptionsPatch = Schema.Struct({
53− reasoningEffort: Schema.optionalKey(CodexModelOptions.fields.reasoningEffort),
54− fastMode: Schema.optionalKey(CodexModelOptions.fields.fastMode),
55−});
56−
57−const 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−
63−const 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−
76−const 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−
82−const ClaudeSettingsPatch = Schema.Struct({
83− binaryPath: Schema.optionalKey(Schema.String),
84− customModels: Schema.optionalKey(Schema.Array(Schema.String)),
85−});
86−
87−export 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−});
98−export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;
7+ DEFAULT_SERVER_SETTINGS,
8+ CodexSettings,
9+ ClaudeSettings,
10+ ServerSettings,
11+ ServerSettingsPatch,
12+ ThreadEnvMode,
13+} from "./settings";
14+
15+export {
16+ DEFAULT_SERVER_SETTINGS,
17+ ServerSettings,
18+ ServerSettingsPatch,
19+ ThreadEnvMode,
20+ CodexSettings,
21+ ClaudeSettings,
22+};
9923
10024 const KeybindingsMalformedConfigIssue = Schema.Struct({
10125 kind: Schema.Literal("keybindings.malformed-config"),
packages/contracts/src/settings.ts+129−0
@@ -0,0 +1,129 @@
1+import * as Schema from "effect/Schema";
2+import { TrimmedNonEmptyString } from "./baseSchemas";
3+import {
4+ ClaudeModelOptions,
5+ CodexModelOptions,
6+ DEFAULT_GIT_TEXT_GENERATION_MODEL_BY_PROVIDER,
7+} from "./model";
8+import { ModelSelection } from "./orchestration";
9+
10+// ── Client Settings (local-only) ───────────────────────────────
11+
12+export const CLIENT_SETTINGS_STORAGE_KEY = "t3code:client-settings:v1";
13+
14+export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
15+export type TimestampFormat = typeof TimestampFormat.Type;
16+export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
17+
18+export const SidebarProjectSortOrder = Schema.Literals(["updated_at", "created_at", "manual"]);
19+export type SidebarProjectSortOrder = typeof SidebarProjectSortOrder.Type;
20+export const DEFAULT_SIDEBAR_PROJECT_SORT_ORDER: SidebarProjectSortOrder = "updated_at";
21+
22+export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at"]);
23+export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
24+export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";
25+
26+export 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+});
37+export type ClientSettings = typeof ClientSettingsSchema.Type;
38+
39+export const DEFAULT_CLIENT_SETTINGS: ClientSettings = Schema.decodeSync(ClientSettingsSchema)({});
40+
41+// ── Server Settings (server-authoritative) ────────────────────
42+
43+export const ThreadEnvMode = Schema.Literals(["local", "worktree"]);
44+export type ThreadEnvMode = typeof ThreadEnvMode.Type;
45+
46+export 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+});
51+export type CodexSettings = typeof CodexSettings.Type;
52+
53+export const ClaudeSettings = Schema.Struct({
54+ binaryPath: Schema.String.pipe(Schema.withDecodingDefault(() => "")),
55+ customModels: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(() => [])),
56+});
57+export type ClaudeSettings = typeof ClaudeSettings.Type;
58+
59+export 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+});
77+export type ServerSettings = typeof ServerSettings.Type;
78+
79+export const DEFAULT_SERVER_SETTINGS: ServerSettings = Schema.decodeSync(ServerSettings)({});
80+
81+// ── Server Settings Patch (replace with a Schema.deepPartial if available) ──────────────────────────────────────────
82+
83+const CodexModelOptionsPatch = Schema.Struct({
84+ reasoningEffort: Schema.optionalKey(CodexModelOptions.fields.reasoningEffort),
85+ fastMode: Schema.optionalKey(CodexModelOptions.fields.fastMode),
86+});
87+
88+const 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+
94+const 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+
107+const 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+
113+const ClaudeSettingsPatch = Schema.Struct({
114+ binaryPath: Schema.optionalKey(Schema.String),
115+ customModels: Schema.optionalKey(Schema.Array(Schema.String)),
116+});
117+
118+export 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+});
129+export type ServerSettingsPatch = typeof ServerSettingsPatch.Type;