From eb461480963ea5ee78c0dd53d3197bde170e238b Mon Sep 17 00:00:00 2001 From: tris203 Date: Wed, 14 Oct 2026 20:25:48 +0000 Subject: [PATCH 1/6] fix(web): storage cleanup rules show where each host's value comes from Storage cleanup rules are fields of the single `storageCleanup` setting, so their rows never got the layers popover other server settings have. Rows now name the rule they edit, and the popover resolves that rule per environment. The redundant "Mixed across selected machines" line is dropped in favour of the mixed layers icon, matching other settings. --- .../settings/SettingInheritance.test.ts | 76 +++++++++++++++++++ .../settings/SettingInheritance.tsx | 66 +++++++++++----- .../components/settings/StorageSettings.tsx | 44 ++++++----- .../components/settings/settingsLayout.tsx | 16 +++- 4 files changed, 164 insertions(+), 38 deletions(-) diff --git a/apps/web/src/components/settings/SettingInheritance.test.ts b/apps/web/src/components/settings/SettingInheritance.test.ts index 3c178cd4c2cfe..c632f73be8320 100644 --- a/apps/web/src/components/settings/SettingInheritance.test.ts +++ b/apps/web/src/components/settings/SettingInheritance.test.ts @@ -99,4 +99,80 @@ describe("settingInheritanceLayers", () => { ["Current checkout", false], ]); }); + + it("resolves one storage cleanup rule per environment", () => { + const settings = { + ...DEFAULT_SERVER_SETTINGS, + storageCleanup: { ...DEFAULT_SERVER_SETTINGS.storageCleanup, logsAfterDays: 30 }, + }; + const layers = (field: "logsAfterDays" | "worktreeOnMerge") => + settingInheritanceLayers( + { + environmentId, + label: "Laptop", + projectId: null, + ...resolveProjectSettings(settings, null), + }, + settings, + "storageCleanup", + field, + ).map((layer) => [layer.label, layer.value, layer.effective]); + expect(layers("logsAfterDays")).toEqual([ + ["Laptop", "30 days", true], + ["Default", "Off", false], + ]); + // Another rule on the same environment is still at its default. + expect(layers("worktreeOnMerge")).toEqual([ + ["Laptop", "Inherits", false], + ["Default", "Off", true], + ]); + }); + + it("reads worktree rules through the environment's cleanup policy", () => { + const layers = ( + worktreeCleanup: (typeof DEFAULT_SERVER_SETTINGS)["worktreeCleanup"], + field: "worktreeOnMerge" | "worktreeKeepWhen", + ) => { + const settings = { + ...DEFAULT_SERVER_SETTINGS, + storageCleanup: { + ...DEFAULT_SERVER_SETTINGS.storageCleanup, + worktreeOnMerge: true, + worktreeKeepWhen: "tracked-changes" as const, + }, + worktreeCleanup, + }; + return settingInheritanceLayers( + { + environmentId, + label: "Laptop", + projectId: null, + ...resolveProjectSettings(settings, null), + }, + settings, + "storageCleanup", + field, + ).map((layer) => [layer.label, layer.value, layer.effective]); + }; + expect(layers({ mode: "off" }, "worktreeOnMerge")).toEqual([ + ["Laptop", "Inherits", false], + ["Default", "Off", true], + ]); + expect( + layers( + { + mode: "custom", + rules: { + ...DEFAULT_SERVER_SETTINGS.storageCleanup, + worktreeOnMerge: false, + worktreeKeepWhen: "any-local-files", + }, + }, + "worktreeKeepWhen", + ), + ).toEqual([ + ["Laptop", "Any local files", true], + ["Default", "Uncommitted changes", false], + ]); + }); }); diff --git a/apps/web/src/components/settings/SettingInheritance.tsx b/apps/web/src/components/settings/SettingInheritance.tsx index 8f83f137241e4..da53f1ec94727 100644 --- a/apps/web/src/components/settings/SettingInheritance.tsx +++ b/apps/web/src/components/settings/SettingInheritance.tsx @@ -4,8 +4,11 @@ import { type ProjectFileBackedSettingKey, resolveEnvironmentMachineKind, type ServerSettings, + type StorageCleanupSettings, + type WorktreeKeepWhen, type WorktreeSubmodules, } from "@t3tools/contracts"; +import { resolveWorktreeCleanup } from "@t3tools/shared/projectSettings"; import { CheckIcon, ChevronRightIcon, LayersIcon } from "lucide-react"; import * as Equal from "effect/Equal"; @@ -31,6 +34,12 @@ interface InheritanceLayer { readonly set: boolean; } +export const WORKTREE_KEEP_WHEN_LABELS = { + "any-local-files": "Any local files", + "uncommitted-changes": "Uncommitted changes", + "tracked-changes": "Edited tracked files", +} as const satisfies Record; + const WRITING_STYLE_LABELS: Record = { repo_conventions: "Repository conventions", conventional_commits: "Conventional Commits", @@ -50,19 +59,21 @@ function formatValue(key: keyof ServerSettings, value: unknown): string { ? "Last selected" : key === "sidebarAutoSettleAfterDays" ? "Never" - : key === "defaultModelSelection" - ? "Automatic" - : key === "sourceControlWriterModelSelection" - ? "Text generation model" - : key === "reviewGuideModelSelection" - ? "Source control writer model" - : key === "defaultThreadEnvMode" || key === "worktreeSubmodules" - ? "Inherit" - : "Not set"; + : key === "storageCleanup" + ? "Off" + : key === "defaultModelSelection" + ? "Automatic" + : key === "sourceControlWriterModelSelection" + ? "Text generation model" + : key === "reviewGuideModelSelection" + ? "Source control writer model" + : key === "defaultThreadEnvMode" || key === "worktreeSubmodules" + ? "Inherit" + : "Not set"; } if (typeof value === "boolean") return value ? "On" : "Off"; if (typeof value === "number") { - return key === "sidebarAutoSettleAfterDays" + return key === "sidebarAutoSettleAfterDays" || key === "storageCleanup" ? `${value} ${value === 1 ? "day" : "days"}` : String(value); } @@ -73,6 +84,9 @@ function formatValue(key: keyof ServerSettings, value: unknown): string { if (key === "worktreeSubmodules" && value in WORKTREE_SUBMODULES_LABELS) { return WORKTREE_SUBMODULES_LABELS[value as WorktreeSubmodules]; } + if (key === "storageCleanup" && value in WORKTREE_KEEP_WHEN_LABELS) { + return WORKTREE_KEEP_WHEN_LABELS[value as WorktreeKeepWhen]; + } if (key === "pullRequestMergeMethod" && value in PULL_REQUEST_MERGE_METHOD_LABELS) { return PULL_REQUEST_MERGE_METHOD_LABELS[ value as keyof typeof PULL_REQUEST_MERGE_METHOD_LABELS @@ -96,15 +110,31 @@ function formatValue(key: keyof ServerSettings, value: unknown): string { return "Custom"; } +/** + * An environment's own value for a setting. `storageCleanup` holds one rule + * per field, so its rows name the field they show. + */ +export function environmentSettingValue( + settings: ServerSettings, + key: keyof ServerSettings, + field?: keyof StorageCleanupSettings, +): unknown { + return key === "storageCleanup" && field + ? { ...settings.storageCleanup, ...resolveWorktreeCleanup(settings, null) }[field] + : settings[key]; +} + /** Matches resolveProjectSettings order. */ export function settingInheritanceLayers( target: ScopedSettingsTarget, environmentSettings: ServerSettings, key: keyof ServerSettings, + field?: keyof StorageCleanupSettings, ): readonly InheritanceLayer[] { - const environmentValue = environmentSettings[key]; + const environmentValue = environmentSettingValue(environmentSettings, key, field); + const defaultValue = environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, field); const source = isProjectScopedSettingKey(key) ? target.sources[key] : "environment"; - const environmentSet = !Equal.equals(environmentValue, DEFAULT_SERVER_SETTINGS[key]); + const environmentSet = !Equal.equals(environmentValue, defaultValue); const fileBacked = isProjectFileBackedSettingKey(key); const layers: InheritanceLayer[] = []; if (target.projectId !== null && isProjectScopedSettingKey(key)) { @@ -133,9 +163,7 @@ export function settingInheritanceLayers( }); } // File-backed defaults come from the resolver, not the schema's decoded null. - const builtIn = fileBacked - ? PROJECT_FILE_BACKED_SETTINGS[key].builtIn - : DEFAULT_SERVER_SETTINGS[key]; + const builtIn = fileBacked ? PROJECT_FILE_BACKED_SETTINGS[key].builtIn : defaultValue; layers.push({ key: "built-in", label: "Default", @@ -170,6 +198,7 @@ export function SettingInheritance({ targets, environments, keys, + field, overridingProjects = [], onClearOverrides, }: { @@ -181,6 +210,7 @@ export function SettingInheritance({ "environmentId" | "label" | "displayUrl" | "serverConfig" >[]; keys: readonly (keyof ServerSettings)[]; + field?: keyof StorageCleanupSettings; overridingProjects?: readonly SettingOverridingProject[]; onClearOverrides?: (entries: readonly ProjectOverrideEntry[]) => void; }) { @@ -204,7 +234,7 @@ export function SettingInheritance({ const secondary = [labels.secondary, path?.split(/[\\/]/).filter(Boolean).slice(-2).join("/")] .filter(Boolean) .join(" ยท "); - const layers = settingInheritanceLayers(target, environment.serverConfig.settings, key); + const layers = settingInheritanceLayers(target, environment.serverConfig.settings, key, field); const winningLayer = layers.find((layer) => layer.effective); return [ { @@ -219,7 +249,9 @@ export function SettingInheritance({ resolution: { winner: winningLayer?.key, layers: layers.map(({ key, value }) => [key, value]), - settings: keys.map((settingKey) => target.settings[settingKey]), + settings: keys.map((settingKey) => + environmentSettingValue(target.settings, settingKey, field), + ), }, }, ]; diff --git a/apps/web/src/components/settings/StorageSettings.tsx b/apps/web/src/components/settings/StorageSettings.tsx index 8be558dca6074..f2098be689a27 100644 --- a/apps/web/src/components/settings/StorageSettings.tsx +++ b/apps/web/src/components/settings/StorageSettings.tsx @@ -36,6 +36,7 @@ import { DatabaseStorageNotice } from "./DatabaseStorageNotice"; import { AdvancedSettings } from "./AdvancedSettings"; import { FoldedSettingsSection } from "./FoldedSettingsSection"; import { SettingsScopeNotice } from "./SettingsScopeNotice"; +import { WORKTREE_KEEP_WHEN_LABELS } from "./SettingInheritance"; import { SettingsGroup } from "./SettingsGroup"; import type { ScopedSettingsTarget } from "./scopedSettings"; import { useSettingsScope } from "./SettingsScopeContext"; @@ -48,6 +49,8 @@ import { } from "./useScopedSettings"; import { settingsScopeEnvironmentLabel } from "./settingsScopeAxis"; +const STORAGE_CLEANUP_KEYS = ["storageCleanup"] as const; + export function WorktreesDirectoryRow() { const { connectedEnvironments, targets } = useSettingsScope(); const settings = useScopedSettings(); @@ -165,12 +168,6 @@ function RetentionControl({ ); } -const KEEP_WHEN_LABELS = { - "any-local-files": "Any local files", - "uncommitted-changes": "Uncommitted changes", - "tracked-changes": "Edited tracked files", -} as const; - function lastRunSummary( entries: readonly { readonly label: string; readonly counts: string }[], machineCount: number, @@ -467,15 +464,18 @@ export function WorktreeCleanupSection() { const mixedModes = targets.some((entry) => projectMode(entry) !== mode); const updateSettings = useUpdateScopedSettings(); const clearSettings = useClearScopedSettings(); - const ruleStatus = (key: keyof StorageCleanupSettings) => - targets.some( + // Each rule is one field of `storageCleanup`; naming it lets the row show + // where that rule's value comes from on every selected machine. + const ruleProps = (key: keyof StorageCleanupSettings) => ({ + mixed: targets.some( (target) => ({ ...target.settings.storageCleanup, ...resolveWorktreeCleanup(target.settings, null) })[ key ] !== settings[key], - ) - ? "Mixed across selected machines" - : undefined; + ), + settingKeys: STORAGE_CLEANUP_KEYS, + settingField: key, + }); const update = (patch: Partial) => updateSettings({ storageCleanup: patch }); const updateWorktree = (patch: Partial) => @@ -556,7 +556,7 @@ export function WorktreeCleanupSection() { <> { if ( next === "any-local-files" || @@ -669,9 +669,9 @@ export function WorktreeCleanupSection() { > - {ruleStatus("worktreeKeepWhen") + {ruleProps("worktreeKeepWhen").mixed ? "Mixed" - : KEEP_WHEN_LABELS[settings.worktreeKeepWhen]} + : WORKTREE_KEEP_WHEN_LABELS[settings.worktreeKeepWhen]} @@ -750,6 +750,9 @@ export function StorageCleanupSections() { Date: Wed, 14 Oct 2026 20:36:13 +0000 Subject: [PATCH 2/6] fix(web): settings layers popover fits storage rule labels The value column truncated at 8rem, cutting keep-when labels such as "Uncommitted changes". Widen the column and the popover so the longest label fits beside the layer name. --- apps/web/src/components/settings/SettingInheritance.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/settings/SettingInheritance.tsx b/apps/web/src/components/settings/SettingInheritance.tsx index da53f1ec94727..edd74cca8e177 100644 --- a/apps/web/src/components/settings/SettingInheritance.tsx +++ b/apps/web/src/components/settings/SettingInheritance.tsx @@ -300,7 +300,7 @@ export function SettingInheritance({ {overrideSummary} - +
{groups.map((members) => { const first = members[0]; @@ -334,7 +334,7 @@ export function SettingInheritance({ : "text-muted-foreground/60", )} > - {layer.value} + {layer.value} {layer.effective ? ( ) : ( @@ -463,7 +463,7 @@ export function SettingInheritance({ ) : null} - + {isProjectScopedSettingKey(key) ? formatValue( key, From cba3489640ef6d69d545759b0a87d5704df0e727 Mon Sep 17 00:00:00 2001 From: tris203 Date: Wed, 14 Oct 2026 20:38:10 +0000 Subject: [PATCH 3/6] fix(web): keep storage mixed hints where no layers icon shows Project-scope rule rows have no layers popover, so they keep the "Mixed across selected machines" text. The popover and row summary now credit an environment whose worktreeCleanup policy overrides a raw rule, even when the result matches the built-in default. --- .../settings/SettingInheritance.test.ts | 5 +++-- .../settings/SettingInheritance.tsx | 21 ++++++++++++++++++- .../components/settings/StorageSettings.tsx | 17 +++++++++------ .../components/settings/settingsLayout.tsx | 14 +++---------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/apps/web/src/components/settings/SettingInheritance.test.ts b/apps/web/src/components/settings/SettingInheritance.test.ts index c632f73be8320..f3d86b7883bc4 100644 --- a/apps/web/src/components/settings/SettingInheritance.test.ts +++ b/apps/web/src/components/settings/SettingInheritance.test.ts @@ -154,9 +154,10 @@ describe("settingInheritanceLayers", () => { field, ).map((layer) => [layer.label, layer.value, layer.effective]); }; + // The policy, not the default, turned the enabled rule off. expect(layers({ mode: "off" }, "worktreeOnMerge")).toEqual([ - ["Laptop", "Inherits", false], - ["Default", "Off", true], + ["Laptop", "Off", true], + ["Default", "Off", false], ]); expect( layers( diff --git a/apps/web/src/components/settings/SettingInheritance.tsx b/apps/web/src/components/settings/SettingInheritance.tsx index edd74cca8e177..838181907d5d9 100644 --- a/apps/web/src/components/settings/SettingInheritance.tsx +++ b/apps/web/src/components/settings/SettingInheritance.tsx @@ -124,6 +124,25 @@ export function environmentSettingValue( : settings[key]; } +/** + * Whether an environment chose its own value. A storage rule also counts when + * the environment's `worktreeCleanup` policy overrides the raw rule, even if + * the result happens to match the built-in default. + */ +export function environmentSettingIsSet( + settings: ServerSettings, + key: keyof ServerSettings, + field?: keyof StorageCleanupSettings, +): boolean { + const value = environmentSettingValue(settings, key, field); + return ( + !Equal.equals(value, environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, field)) || + (key === "storageCleanup" && + field !== undefined && + !Equal.equals(value, settings.storageCleanup[field])) + ); +} + /** Matches resolveProjectSettings order. */ export function settingInheritanceLayers( target: ScopedSettingsTarget, @@ -134,7 +153,7 @@ export function settingInheritanceLayers( const environmentValue = environmentSettingValue(environmentSettings, key, field); const defaultValue = environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, field); const source = isProjectScopedSettingKey(key) ? target.sources[key] : "environment"; - const environmentSet = !Equal.equals(environmentValue, defaultValue); + const environmentSet = environmentSettingIsSet(environmentSettings, key, field); const fileBacked = isProjectFileBackedSettingKey(key); const layers: InheritanceLayer[] = []; if (target.projectId !== null && isProjectScopedSettingKey(key)) { diff --git a/apps/web/src/components/settings/StorageSettings.tsx b/apps/web/src/components/settings/StorageSettings.tsx index f2098be689a27..65a96c4a7bb13 100644 --- a/apps/web/src/components/settings/StorageSettings.tsx +++ b/apps/web/src/components/settings/StorageSettings.tsx @@ -466,16 +466,21 @@ export function WorktreeCleanupSection() { const clearSettings = useClearScopedSettings(); // Each rule is one field of `storageCleanup`; naming it lets the row show // where that rule's value comes from on every selected machine. - const ruleProps = (key: keyof StorageCleanupSettings) => ({ - mixed: targets.some( + const ruleProps = (key: keyof StorageCleanupSettings) => { + const mixed = targets.some( (target) => ({ ...target.settings.storageCleanup, ...resolveWorktreeCleanup(target.settings, null) })[ key ] !== settings[key], - ), - settingKeys: STORAGE_CLEANUP_KEYS, - settingField: key, - }); + ); + return { + mixed, + // Project rows have no layers icon, so they keep the mixed hint as text. + status: isProjectScope && mixed ? "Mixed across selected machines" : undefined, + settingKeys: STORAGE_CLEANUP_KEYS, + settingField: key, + }; + }; const update = (patch: Partial) => updateSettings({ storageCleanup: patch }); const updateWorktree = (patch: Partial) => diff --git a/apps/web/src/components/settings/settingsLayout.tsx b/apps/web/src/components/settings/settingsLayout.tsx index ada4adce350cc..35cf1c59b5872 100644 --- a/apps/web/src/components/settings/settingsLayout.tsx +++ b/apps/web/src/components/settings/settingsLayout.tsx @@ -3,12 +3,7 @@ import { AuthSettingsWriteScope } from "@t3tools/contracts"; import { usePrimaryEnvironmentId, usePrimaryEnvironment } from "../../state/environments"; import { useEnvironmentScope, useEnvironmentsWithScope } from "../../state/session"; import { InfoIcon, Undo2Icon } from "lucide-react"; -import { - DEFAULT_SERVER_SETTINGS, - type ServerSettings, - type StorageCleanupSettings, -} from "@t3tools/contracts"; -import * as Equal from "effect/Equal"; +import { type ServerSettings, type StorageCleanupSettings } from "@t3tools/contracts"; import { useLocation, useNavigate } from "@tanstack/react-router"; import { createContext, type ComponentPropsWithoutRef, type ReactNode, useContext } from "react"; @@ -31,7 +26,7 @@ import { } from "./scopedSettings"; import { useClearProjectOverrides, useClearScopedSettings } from "./useScopedSettings"; import { - environmentSettingValue, + environmentSettingIsSet, SettingInheritance, type SettingInheritanceState, type SettingOverridingProject, @@ -426,10 +421,7 @@ export function SettingsRow({ const environmentSettings = environmentSettingsById.get(candidate.environmentId); return ( environmentSettings !== undefined && - !Equal.equals( - environmentSettingValue(environmentSettings, key, settingField), - environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, settingField), - ) + environmentSettingIsSet(environmentSettings, key, settingField) ); }), ); From 2efb9ece7c60e3e414e14320695462e447b5821f Mon Sep 17 00:00:00 2001 From: tris203 Date: Wed, 14 Oct 2026 20:43:31 +0000 Subject: [PATCH 4/6] fix(web): keep the storage value helper private Only environmentSettingIsSet is used outside SettingInheritance now, so knip flagged the exported environmentSettingValue. --- apps/web/src/components/settings/SettingInheritance.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/SettingInheritance.tsx b/apps/web/src/components/settings/SettingInheritance.tsx index 838181907d5d9..fe3842c85ac64 100644 --- a/apps/web/src/components/settings/SettingInheritance.tsx +++ b/apps/web/src/components/settings/SettingInheritance.tsx @@ -114,7 +114,7 @@ function formatValue(key: keyof ServerSettings, value: unknown): string { * An environment's own value for a setting. `storageCleanup` holds one rule * per field, so its rows name the field they show. */ -export function environmentSettingValue( +function environmentSettingValue( settings: ServerSettings, key: keyof ServerSettings, field?: keyof StorageCleanupSettings, From dcfc771f6ff8c038741981116b7cf335b4406be3 Mon Sep 17 00:00:00 2001 From: tris203 Date: Thu, 15 Oct 2026 21:59:38 +0000 Subject: [PATCH 5/6] fix(web): apply storage inheritance to settled worktree cleanup --- apps/web/src/components/settings/StorageSettings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/StorageSettings.tsx b/apps/web/src/components/settings/StorageSettings.tsx index 65a96c4a7bb13..69d959b8d9724 100644 --- a/apps/web/src/components/settings/StorageSettings.tsx +++ b/apps/web/src/components/settings/StorageSettings.tsx @@ -610,8 +610,9 @@ export function WorktreeCleanupSection() { ) && ( connectedEnvironments.length ? "Offline machines will not get this setting" : undefined, From 991f7193b4d0c5944264573afd77f235036cc9ff Mon Sep 17 00:00:00 2001 From: tris203 Date: Tue, 27 Oct 2026 23:16:49 +0000 Subject: [PATCH 6/6] fix(web): storage rules credit a cleanup policy that matches the default An environment's worktreeCleanup policy decides its worktree rules even when the result equals both the raw rule and the built-in default. The row and popover now credit the environment in that case instead of showing the default. --- .../settings/SettingInheritance.test.ts | 33 +++++++++++++++++++ .../settings/SettingInheritance.tsx | 16 ++++++--- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/settings/SettingInheritance.test.ts b/apps/web/src/components/settings/SettingInheritance.test.ts index f3d86b7883bc4..3a5f06c4bce6f 100644 --- a/apps/web/src/components/settings/SettingInheritance.test.ts +++ b/apps/web/src/components/settings/SettingInheritance.test.ts @@ -176,4 +176,37 @@ describe("settingInheritanceLayers", () => { ["Default", "Uncommitted changes", false], ]); }); + + it("credits the environment's cleanup policy for rules that match the default", () => { + const layers = ( + worktreeCleanup: (typeof DEFAULT_SERVER_SETTINGS)["worktreeCleanup"], + field: "worktreeOnMerge" | "worktreeKeepWhen", + ) => { + const settings = { ...DEFAULT_SERVER_SETTINGS, worktreeCleanup }; + return settingInheritanceLayers( + { + environmentId, + label: "Laptop", + projectId: null, + ...resolveProjectSettings(settings, null), + }, + settings, + "storageCleanup", + field, + ).map((layer) => [layer.label, layer.value, layer.effective]); + }; + const policyWins = [ + ["Laptop", "Off", true], + ["Default", "Off", false], + ]; + expect(layers({ mode: "off" }, "worktreeOnMerge")).toEqual(policyWins); + expect( + layers({ mode: "custom", rules: DEFAULT_SERVER_SETTINGS.storageCleanup }, "worktreeOnMerge"), + ).toEqual(policyWins); + // An off policy keeps the environment's own keep-when rule. + expect(layers({ mode: "off" }, "worktreeKeepWhen")).toEqual([ + ["Laptop", "Inherits", false], + ["Default", "Uncommitted changes", true], + ]); + }); }); diff --git a/apps/web/src/components/settings/SettingInheritance.tsx b/apps/web/src/components/settings/SettingInheritance.tsx index fe3842c85ac64..7a18b37ba1bfe 100644 --- a/apps/web/src/components/settings/SettingInheritance.tsx +++ b/apps/web/src/components/settings/SettingInheritance.tsx @@ -126,20 +126,26 @@ function environmentSettingValue( /** * Whether an environment chose its own value. A storage rule also counts when - * the environment's `worktreeCleanup` policy overrides the raw rule, even if - * the result happens to match the built-in default. + * the environment's `worktreeCleanup` policy decides it, even if the result + * happens to match the built-in default. */ export function environmentSettingIsSet( settings: ServerSettings, key: keyof ServerSettings, field?: keyof StorageCleanupSettings, ): boolean { - const value = environmentSettingValue(settings, key, field); + const policy = settings.worktreeCleanup; return ( - !Equal.equals(value, environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, field)) || + !Equal.equals( + environmentSettingValue(settings, key, field), + environmentSettingValue(DEFAULT_SERVER_SETTINGS, key, field), + ) || (key === "storageCleanup" && field !== undefined && - !Equal.equals(value, settings.storageCleanup[field])) + policy !== null && + field in resolveWorktreeCleanup(settings, null) && + // An off policy still reads keep-when from `storageCleanup`. + !(policy.mode === "off" && field === "worktreeKeepWhen")) ); }