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 @@
1import { Schema } from "effect";
1import { describe, expect, it } from "vitest";2import { describe, expect, it } from "vitest";
2 3
3import {4import {
5 AppSettingsSchema,
4 DEFAULT_TIMESTAMP_FORMAT,6 DEFAULT_TIMESTAMP_FORMAT,
5 getAppModelOptions,7 getAppModelOptions,
6 normalizeCustomModelSlugs,8 normalizeCustomModelSlugs,
@@ -87,3 +89,27 @@ describe("provider-specific custom models", () => { · 80 unmodified lines
87 expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);89 expect(claudeOptions.some((option) => option.slug === "claude/custom-opus")).toBe(true);
88 });90 });
89});91});
92
93describe("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
3import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";3import { TrimmedNonEmptyString, type ProviderKind } from "@t3tools/contracts";
4import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";4import { getDefaultModel, getModelOptions, normalizeModelSlug } from "@t3tools/shared/model";
5import { useLocalStorage } from "./hooks/useLocalStorage";5import { useLocalStorage } from "./hooks/useLocalStorage";
6import { EnvMode } from "./components/BranchToolbar.logic";
6 7
7const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";8const APP_SETTINGS_STORAGE_KEY = "t3code:app-settings:v1";
8const MAX_CUSTOM_MODEL_COUNT = 32;9const MAX_CUSTOM_MODEL_COUNT = 32;
9export const MAX_CUSTOM_MODEL_LENGTH = 256;10export const MAX_CUSTOM_MODEL_LENGTH = 256;
10export const TIMESTAMP_FORMAT_OPTIONS = ["locale", "12-hour", "24-hour"] as const;11
11export type TimestampFormat = (typeof TIMESTAMP_FORMAT_OPTIONS)[number];12export const TimestampFormat = Schema.Literals(["locale", "12-hour", "24-hour"]);
13export type TimestampFormat = typeof TimestampFormat.Type;
12export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";14export const DEFAULT_TIMESTAMP_FORMAT: TimestampFormat = "locale";
15
13const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {16const BUILT_IN_MODEL_SLUGS_BY_PROVIDER: Record<ProviderKind, ReadonlySet<string>> = {
14 codex: new Set(getModelOptions("codex").map((option) => option.slug)),17 codex: new Set(getModelOptions("codex").map((option) => option.slug)),
15 claudeAgent: new Set(getModelOptions("claudeAgent").map((option) => option.slug)),18 claudeAgent: new Set(getModelOptions("claudeAgent").map((option) => option.slug)),
16};19};
17 20
18const AppSettingsSchema = Schema.Struct({21const withDefaults =
19 codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(22 <
20 Schema.withConstructorDefault(() => Option.some("")),23 S extends Schema.Top & Schema.WithoutConstructorDefault,
21 ),24 D extends S["~type.make.in"] & S["Encoded"],
22 codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(25 >(
23 Schema.withConstructorDefault(() => Option.some("")),26 fallback: () => D,
24 ),27 ) =>
25 defaultThreadEnvMode: Schema.Literals(["local", "worktree"]).pipe(28 (schema: S) =>
26 Schema.withConstructorDefault(() => Option.some("local")),29 schema.pipe(
27 ),30 Schema.withConstructorDefault(() => Option.some(fallback())),
28 confirmThreadDelete: Schema.Boolean.pipe(Schema.withConstructorDefault(() => Option.some(true))),31 Schema.withDecodingDefault(() => fallback()),
29 enableAssistantStreaming: Schema.Boolean.pipe(32 );
30 Schema.withConstructorDefault(() => Option.some(false)),33
31 ),34export const AppSettingsSchema = Schema.Struct({
32 timestampFormat: Schema.Literals(["locale", "12-hour", "24-hour"]).pipe(35 codexBinaryPath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
33 Schema.withConstructorDefault(() => Option.some(DEFAULT_TIMESTAMP_FORMAT)),36 codexHomePath: Schema.String.check(Schema.isMaxLength(4096)).pipe(withDefaults(() => "")),
34 ),37 defaultThreadEnvMode: EnvMode.pipe(withDefaults(() => "local" as const satisfies EnvMode)),
35 customCodexModels: Schema.Array(Schema.String).pipe(38 confirmThreadDelete: Schema.Boolean.pipe(withDefaults(() => true)),
36 Schema.withConstructorDefault(() => Option.some([])),39 enableAssistantStreaming: Schema.Boolean.pipe(withDefaults(() => false)),
37 ),40 timestampFormat: TimestampFormat.pipe(withDefaults(() => DEFAULT_TIMESTAMP_FORMAT)),
38 customClaudeModels: Schema.Array(Schema.String).pipe(41 customCodexModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
39 Schema.withConstructorDefault(() => Option.some([])),42 customClaudeModels: Schema.Array(Schema.String).pipe(withDefaults(() => [])),
40 ),
41 textGenerationModel: Schema.optional(TrimmedNonEmptyString),43 textGenerationModel: Schema.optional(TrimmedNonEmptyString),
42});44});
43export type AppSettings = typeof AppSettingsSchema.Type;45export type AppSettings = typeof AppSettingsSchema.Type;
apps/web/src/components/BranchToolbar.logic.ts+3−1
@@ -1,6 +1,8 @@
1import type { GitBranch } from "@t3tools/contracts";1import type { GitBranch } from "@t3tools/contracts";
2import { Schema } from "effect";
2 3
3export type EnvMode = "local" | "worktree";4export const EnvMode = Schema.Literals(["local", "worktree"]);
5export type EnvMode = typeof EnvMode.Type;
4 6
5export function resolveEffectiveEnvMode(input: {7export function resolveEffectiveEnvMode(input: {
6 activeWorktreePath: string | null;8 activeWorktreePath: string | null;