History

Backfill missing app settings defaults during decode

- add decoding defaults in `AppSettingsSchema` so older persisted settings load safely - export shared `Schema.Literals` types for `EnvMode` and `TimestampFormat` - add a regression test covering pre-new-key settings hydration

Julius Marminge committed 7 months ago

Commit 654ca0d1 parent aa66e083 files · +56 −26Browse files

Unified · Split

3 files
  1. apps/web/src/appSettings.test.ts +26 −0
  2. apps/web/src/appSettings.ts +27 −25
  3. apps/web/src/components/BranchToolbar.logic.ts +3 −1
apps/web/src/appSettings.test.ts+26−0
@@ -1,6 +1,8 @@
1+import { Schema } from "effect";
12 import { describe, expect, it } from "vitest";
23
34 import {
5+ AppSettingsSchema,
46 DEFAULT_TIMESTAMP_FORMAT,
57 getAppModelOptions,
68 normalizeCustomModelSlugs,
@@ -87,3 +89,27 @@ describe("provider-specific custom models", () => { · 80 unmodified lines
8789 expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);
8890 });
8991 });
92+
93+describe("AppSettingsSchema", () => {
94+ it("fills decoding defaults for persisted settings that predate newer keys", () => {
95+ const decode = Schema.decodeSync(Schema.fromJsonString(AppSettingsSchema));
96+
97+ expect(
98+ decode(
99+ JSON.stringify({
100+ codexBinaryPath: "/usr/local/bin/codex",
101+ confirmThreadDelete: false,
102+ }),
103+ ),
104+ ).toMatchObject({
105+ codexBinaryPath: "/usr/local/bin/codex",
106+ codexHomePath: "",
107+ defaultThreadEnvMode: "local",
108+ confirmThreadDelete: false,
109+ enableAssistantStreaming: false,
110+ timestampFormat: DEFAULT_TIMESTAMP_FORMAT,
111+ customCodexModels: [],
112+ customClaudeModels: [],
113+ });
114+ });
115+});
apps/web/src/appSettings.ts+27−25
@@ -3,41 +3,43 @@ import { Option, Schema } from "effect"; · 2 unmodified lines
33 import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";
44 import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
55 import { useLocalStorage } from "./hooks/useLocalStorage";
6+import { EnvMode } from "./components/BranchToolbar.logic";
67
78 const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
89 const MAX_CUSTOM_MODEL_COUNT = 32;
910 export const MAX_CUSTOM_MODEL_LENGTH = 256;
10−export const TIMESTAMP_FORMAT_OPTIONS = ["locale", "12-hour", "24-hour"] as const;
11−export type TimestampFormat = (typeof TIMESTAMP_FORMAT_OPTIONS)[number];
11+
12+export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
13+export type TimestampFormat = typeof TimestampFormat.Type;
1214 export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
15+
1316 const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {
1417 codex: new Set(getModelOptions("codex").map((option) => option.slug)),
1518 claudeAgent: new Set(getModelOptions("claudeAgent").map((option) => option.slug)),
1619 };
1720
18−const AppSettingsSchema = Schema.Struct({
19− codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
20− Schema.withConstructorDefault(() => Option.some("")),
21− ),
22− codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(
23− Schema.withConstructorDefault(() => Option.some("")),
24− ),
25− defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe(
26− Schema.withConstructorDefault(() => Option.some("local")),
27− ),
28− confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),
29− enableAssistantStreaming: Schema.Boolean.pipe(
30− Schema.withConstructorDefault(() => Option.some(false)),
31− ),
32− timestampFormat: Schema.Literals(["locale", "12-hour", "24-hour"]).pipe(
33− Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)),
34− ),
35− customCodexModels: Schema.Array(Schema.String).pipe(
36− Schema.withConstructorDefault(() => Option.some([])),
37− ),
38− customClaudeModels: Schema.Array(Schema.String).pipe(
39− Schema.withConstructorDefault(() => Option.some([])),
40− ),
21+const withDefaults =
22+ <
23+ S extends Schema.Top & Schema.WithoutConstructorDefault,
24+ D extends S["~type.make.in"] & S["Encoded"],
25+ >(
26+ fallback: () => D,
27+ ) =>
28+ (schema: S) =>
29+ schema.pipe(
30+ Schema.withConstructorDefault(() => Option.some(fallback())),
31+ Schema.withDecodingDefault(() => fallback()),
32+ );
33+
34+export const AppSettingsSchema = Schema.Struct({
35+ codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
36+ codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
37+ defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "local" as const satisfies EnvMode)),
38+ confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
39+ enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
40+ timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)),
41+ customCodexModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
42+ customClaudeModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
4143 textGenerationModel: Schema.optional(TrimmedNonEmptyString),
4244 });
4345 export type AppSettings = typeof AppSettingsSchema.Type;
apps/web/src/components/BranchToolbar.logic.ts+3−1
@@ -1,6 +1,8 @@
11 import type { GitBranch } from "@t3tools/contracts";
2+import { Schema } from "effect";
23
3−export type EnvMode = "local" | "worktree";
4+export const EnvMode = Schema.Literals(["local", "worktree"]);
5+export type EnvMode = typeof EnvMode.Type;
46
57 export function resolveEffectiveEnvMode(input: {
68 activeWorktreePath: string | null;