Unified · Split
| @@ -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({}); | ||
| @@ -31,7 +31,7 @@ import { isMacPlatform } from "../lib/utils"; · 30 unmodified lines | |||
| 31 | import { getRouter } from "../router"; | 31 | import { getRouter } from "../router"; |
| 32 | import { useStore } from "../store"; | 32 | import { useStore } from "../store"; |
| 33 | import { estimateTimelineMessageHeight } from "./timelineHeight"; | 33 | import { estimateTimelineMessageHeight } from "./timelineHeight"; |
| 34 | import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings"; | 34 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; |
| 35 | 35 | ||
| 36 | const THREAD_ID = "thread-browser-test" as ThreadId; | 36 | const THREAD_ID = "thread-browser-test" as ThreadId; |
| 37 | const UUID_ROUTE_RE = /^\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; | 37 | const UUID_ROUTE_RE = /^\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; |
| @@ -1,5 +1,5 @@ | |||
| 1 | import { memo, useState, useCallback } from "react"; | 1 | import { memo, useState, useCallback } from "react"; |
| 2 | import { type TimestampFormat } from "../clientSettings"; | 2 | import { type TimestampFormat } from "@t3tools/contracts/settings"; |
| 3 | import { Badge } from "./ui/badge"; | 3 | import { Badge } from "./ui/badge"; |
| 4 | import { Button } from "./ui/button"; | 4 | import { Button } from "./ui/button"; |
| 5 | import { ScrollArea } from "./ui/scroll-area"; | 5 | import { ScrollArea } from "./ui/scroll-area"; |
| @@ -1,4 +1,4 @@ | |||
| 1 | import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "../clientSettings"; | 1 | import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings"; |
| 2 | import type { Thread } from "../types"; | 2 | import type { Thread } from "../types"; |
| 3 | import { cn } from "../lib/utils"; | 3 | import { cn } from "../lib/utils"; |
| 4 | import { | 4 | import { |
| @@ -38,7 +38,10 @@ import { · 37 unmodified lines | |||
| 38 | } from "@t3tools/contracts"; | 38 | } from "@t3tools/contracts"; |
| 39 | import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; | 39 | import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 40 | import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; | 40 | 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"; | ||
| 42 | import { isElectron } from "../env"; | 45 | import { isElectron } from "../env"; |
| 43 | import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; | 46 | import { APP_STAGE_LABEL, APP_VERSION } from "../branding"; |
| 44 | import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils"; | 47 | import { isLinuxPlatform, isMacPlatform, newCommandId, newProjectId } from "../lib/utils"; |
| @@ -48,7 +48,7 @@ import { · 47 unmodified lines | |||
| 48 | type ParsedTerminalContextEntry, | 48 | type ParsedTerminalContextEntry, |
| 49 | } from "~/lib/terminalContext"; | 49 | } from "~/lib/terminalContext"; |
| 50 | import { cn } from "~/lib/utils"; | 50 | import { cn } from "~/lib/utils"; |
| 51 | import { type TimestampFormat } from "../../clientSettings"; | 51 | import { type TimestampFormat } from "@t3tools/contracts/settings"; |
| 52 | import { formatTimestamp } from "../../timestampFormat"; | 52 | import { formatTimestamp } from "../../timestampFormat"; |
| 53 | import { | 53 | import { |
| 54 | buildInlineTerminalContextText, | 54 | buildInlineTerminalContextText, |
| @@ -22,7 +22,7 @@ import { · 21 unmodified lines | |||
| 22 | useComposerThreadDraft, | 22 | useComposerThreadDraft, |
| 23 | useEffectiveComposerModelState, | 23 | useEffectiveComposerModelState, |
| 24 | } from "../../composerDraftStore"; | 24 | } from "../../composerDraftStore"; |
| 25 | import { DEFAULT_CLIENT_SETTINGS } from "~/clientSettings"; | 25 | import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; |
| 26 | 26 | ||
| 27 | // ── Claude TraitsPicker tests ───────────────────────────────────────── | 27 | // ── Claude TraitsPicker tests ───────────────────────────────────────── |
| 28 | 28 | ||
| @@ -19,16 +19,19 @@ import { · 18 unmodified lines | |||
| 19 | ThreadEnvMode, | 19 | ThreadEnvMode, |
| 20 | } from "@t3tools/contracts"; | 20 | } from "@t3tools/contracts"; |
| 21 | import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts"; | 21 | 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"; | ||
| 22 | import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; | 31 | import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; |
| 23 | import { ensureNativeApi } from "~/nativeApi"; | 32 | import { ensureNativeApi } from "~/nativeApi"; |
| 24 | import { useLocalStorage } from "./useLocalStorage"; | 33 | import { useLocalStorage } from "./useLocalStorage"; |
| 25 | import { normalizeCustomModelSlugs } from "~/modelSelection"; | 34 | import { normalizeCustomModelSlugs } from "~/modelSelection"; |
| 26 | import { | ||
| 27 | ClientSettingsSchema, | ||
| 28 | type ClientSettings, | ||
| 29 | DEFAULT_CLIENT_SETTINGS, | ||
| 30 | CLIENT_SETTINGS_STORAGE_KEY, | ||
| 31 | } from "~/clientSettings"; | ||
| 32 | import { Predicate, Schema, Struct } from "effect"; | 35 | import { Predicate, Schema, Struct } from "effect"; |
| 33 | import { DeepMutable } from "effect/Types"; | 36 | import { DeepMutable } from "effect/Types"; |
| 34 | import { deepMerge } from "@t3tools/shared/Struct"; | 37 | import { 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 | ||
| 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 | |||
| 202 | /** | 233 | /** |
| 203 | * Call once on app startup. Migrates server-relevant settings from the | 234 | * 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 | */ |
| 206 | export function migrateLocalSettingsToServer(): void { | 238 | export 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 retrying | 274 | // Mark as migrated no matter what to avoid retrying |
| 235 | localStorage.setItem(MIGRATION_FLAG_KEY, "true"); | 275 | localStorage.setItem(MIGRATION_FLAG_KEY, "true"); |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { type TimestampFormat } from "./clientSettings"; | 1 | import { type TimestampFormat } from "@t3tools/contracts/settings"; |
| 2 | 2 | ||
| 3 | export function getTimestampFormatOptions( | 3 | export function getTimestampFormatOptions( |
| 4 | timestampFormat: TimestampFormat, | 4 | timestampFormat: TimestampFormat, |
| @@ -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", |
| @@ -2,100 +2,24 @@ import { Schema } from "effect"; · 1 unmodified line | |||
| 2 | import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas"; | 2 | import { IsoDateTime, TrimmedNonEmptyString } from "./baseSchemas"; |
| 3 | import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; | 3 | import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; |
| 4 | import { EditorId } from "./editor"; | 4 | import { EditorId } from "./editor"; |
| 5 | import { ProviderKind } from "./orchestration"; | ||
| 5 | import { | 6 | import { |
| 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, |
| 10 | import { ModelSelection, ProviderKind } from "./orchestration"; | 11 | ServerSettingsPatch, |
| 11 | 12 | ThreadEnvMode, | |
| 12 | // ── Server Settings (server-authoritative) ─────────────────────────── | 13 | } from "./settings"; |
| 13 | 14 | ||
| 14 | export const ThreadEnvMode = Schema.Literals(["local", "worktree"]); | 15 | export { |
| 15 | export type ThreadEnvMode = typeof ThreadEnvMode.Type; | 16 | DEFAULT_SERVER_SETTINGS, |
| 16 | 17 | ServerSettings, | |
| 17 | export 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 | }; |
| 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; | ||
| 99 | 23 | ||
| 100 | const KeybindingsMalformedConfigIssue = Schema.Struct({ | 24 | const KeybindingsMalformedConfigIssue = Schema.Struct({ |
| 101 | kind: Schema.Literal("keybindings.malformed-config"), | 25 | kind: Schema.Literal("keybindings.malformed-config"), |
| @@ -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; | ||