Skip to content

Commit 8efd4e9

Browse files
t3dotggclaude
andauthored
fix(settings): sync auto-settle and other shared preferences across environments (pingdotgg#9147)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5014e5f commit 8efd4e9

10 files changed

Lines changed: 489 additions & 44 deletions

File tree

‎apps/mobile/src/features/settings/SettingsRouteScreen.tsx‎

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
squashAtomCommandFailure,
2020
} from "@t3tools/client-runtime/state/runtime";
2121
import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
22-
import { AppText as Text } from "../../components/AppText";
22+
import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
2323
import { supportsAgentAwarenessPush } from "../agent-awareness/capabilities";
2424
import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences";
2525
import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions";
@@ -36,7 +36,17 @@ import { runtime } from "../../lib/runtime";
3636
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
3737
import { serverEnvironment } from "../../state/server";
3838
import { useAtomCommand } from "../../state/use-atom-command";
39-
import type { EnvironmentId } from "@t3tools/contracts";
39+
import { useEnvironments } from "../../state/environments";
40+
import {
41+
DEFAULT_SERVER_SETTINGS,
42+
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
43+
MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
44+
type ServerSettingsPatch,
45+
} from "@t3tools/contracts";
46+
import {
47+
findSharedSettingsMismatches,
48+
pickSharedServerSettings,
49+
} from "@t3tools/client-runtime/state/shared-settings";
4050
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
4151
import {
4252
type AppUpdateCheckState,
@@ -530,51 +540,136 @@ function ConfiguredSettingsRouteScreen() {
530540
}
531541

532542
function GeneralSettingsSection() {
533-
const { savedConnectionsById } = useSavedRemoteConnections();
534-
const connections = Object.values(savedConnectionsById).sort((left, right) =>
535-
left.environmentLabel.localeCompare(right.environmentLabel),
536-
);
537-
538543
return (
539544
<SettingsSection title="General">
540545
<SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" />
541-
{connections.map((connection) => (
542-
<EnvironmentAutoSettleSwitch
543-
key={connection.environmentId}
544-
environmentId={connection.environmentId}
545-
environmentLabel={connection.environmentLabel}
546-
/>
547-
))}
546+
<AutoSettleSettingsRows />
548547
<SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" />
549548
</SettingsSection>
550549
);
551550
}
552551

553-
function EnvironmentAutoSettleSwitch(props: {
554-
readonly environmentId: EnvironmentId;
555-
readonly environmentLabel: string;
556-
}) {
557-
const settings = useAtomValue(serverEnvironment.settingsValueAtom(props.environmentId));
558-
const config = useAtomValue(serverEnvironment.configValueAtom(props.environmentId));
552+
const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterDays ?? 3;
553+
554+
/**
555+
* Auto-settlement is a user preference that every server has to hold. Mobile
556+
* has no primary environment, so the first connected environment that
557+
* supports it is the reference value. Edits fan out to every connected
558+
* environment, and a mismatch row lets the user push the reference out.
559+
*/
560+
function AutoSettleSettingsRows() {
561+
const { environments } = useEnvironments();
559562
const updateSettings = useAtomCommand(serverEnvironment.updateSettings, {
560-
label: "auto-settle settings update",
563+
label: "server settings update",
561564
reportFailure: true,
562565
});
563-
if (config?.environment.capabilities.threadAutoSettlement !== true || settings === null) {
566+
567+
const connected = environments.filter(
568+
(environment) =>
569+
environment.connection.phase === "connected" &&
570+
environment.serverConfig?.environment.capabilities.threadAutoSettlement === true,
571+
);
572+
const reference = connected[0] ?? null;
573+
const referenceSettings = reference?.serverConfig?.settings ?? null;
574+
575+
const [daysDraft, setDaysDraft] = useState<string | null>(null);
576+
577+
if (reference === null || referenceSettings === null) {
564578
return null;
565579
}
580+
581+
const writeToAll = (patch: ServerSettingsPatch) => {
582+
for (const environment of connected) {
583+
void updateSettings({ environmentId: environment.environmentId, input: { patch } });
584+
}
585+
};
586+
587+
const mismatches = findSharedSettingsMismatches({
588+
primaryEnvironmentId: reference.environmentId,
589+
primarySettings: referenceSettings,
590+
environments: environments.map((environment) => ({
591+
environmentId: environment.environmentId,
592+
label: environment.label,
593+
connected: environment.connection.phase === "connected",
594+
settings: environment.serverConfig?.settings ?? null,
595+
})),
596+
});
597+
598+
const afterDays = referenceSettings.sidebarAutoSettleAfterDays;
599+
const commitDays = () => {
600+
const draft = (daysDraft ?? "").trim();
601+
setDaysDraft(null);
602+
// Whole-string check so "3.5" and "3days" are rejected instead of
603+
// silently becoming 3 on every connected environment.
604+
const parsed = /^\d+$/.test(draft) ? Number(draft) : Number.NaN;
605+
if (
606+
Number.isInteger(parsed) &&
607+
parsed >= MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS &&
608+
parsed <= MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS &&
609+
parsed !== afterDays
610+
) {
611+
writeToAll({ sidebarAutoSettleAfterDays: parsed });
612+
}
613+
};
614+
566615
return (
567-
<SettingsSwitchRow
568-
icon="arrow.triangle.branch"
569-
label={`Auto-settle merged threads · ${props.environmentLabel}`}
570-
value={settings?.sidebarAutoSettleOnMerge ?? true}
571-
onValueChange={(value) => {
572-
void updateSettings({
573-
environmentId: props.environmentId,
574-
input: { patch: { sidebarAutoSettleOnMerge: value } },
575-
});
576-
}}
577-
/>
616+
<>
617+
<SettingsSwitchRow
618+
icon="arrow.triangle.branch"
619+
label="Auto-settle merged threads"
620+
value={referenceSettings.sidebarAutoSettleOnMerge}
621+
onValueChange={(value) => writeToAll({ sidebarAutoSettleOnMerge: value })}
622+
/>
623+
<SettingsSwitchRow
624+
icon="clock"
625+
label="Auto-settle inactive threads"
626+
subtitle={afterDays === null ? undefined : `After ${afterDays} days without activity`}
627+
value={afterDays !== null}
628+
onValueChange={(value) =>
629+
writeToAll({ sidebarAutoSettleAfterDays: value ? AUTO_SETTLE_DEFAULT_DAYS : null })
630+
}
631+
/>
632+
{afterDays !== null ? (
633+
<View className="flex-row items-center gap-4 border-t border-border-subtle p-4">
634+
<Text className="flex-1 text-lg text-foreground">Days before auto-settle</Text>
635+
<TextInput
636+
className="min-h-10 w-20 rounded-xl px-3 py-2 text-center text-base"
637+
keyboardType="number-pad"
638+
returnKeyType="done"
639+
value={daysDraft ?? String(afterDays)}
640+
onChangeText={setDaysDraft}
641+
onBlur={commitDays}
642+
onSubmitEditing={commitDays}
643+
accessibilityLabel="Days before auto-settle"
644+
/>
645+
</View>
646+
) : null}
647+
{mismatches.length > 0 ? (
648+
<View className="flex-row items-center gap-4 border-t border-border-subtle p-4">
649+
<View className="min-w-0 flex-1">
650+
<Text className="text-lg text-foreground">Settings differ</Text>
651+
<Text className="text-sm text-foreground-muted">
652+
{mismatches.map((mismatch) => mismatch.label).join(", ")}
653+
</Text>
654+
</View>
655+
<Pressable
656+
accessibilityRole="button"
657+
onPress={() => {
658+
const patch = pickSharedServerSettings(referenceSettings);
659+
for (const mismatch of mismatches) {
660+
void updateSettings({
661+
environmentId: mismatch.environmentId,
662+
input: { patch },
663+
});
664+
}
665+
}}
666+
className="rounded-full bg-subtle px-4 py-2 active:opacity-70"
667+
>
668+
<Text className="text-base font-t3-medium text-foreground">Apply to all</Text>
669+
</Pressable>
670+
</View>
671+
) : null}
672+
</>
578673
);
579674
}
580675

‎apps/web/src/components/settings/SettingsPanels.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import {
112112
TYPOGRAPHY_ADVANCED_STORAGE_KEY,
113113
} from "../../appearanceFonts";
114114
import { CodeFontPreview, PromptFontPreview, TerminalFontPreview } from "./SettingsFontPreviews";
115+
import { SharedSettingsMismatchAlert } from "./SharedSettingsMismatchAlert";
115116
import { discoverInstalledFonts, FontFamilyPicker, useFontEnumeration } from "./FontFamilyPicker";
116117
import {
117118
NumberField,
@@ -1928,6 +1929,7 @@ export function GeneralSettingsPanel() {
19281929

19291930
return (
19301931
<SettingsPageContainer>
1932+
<SharedSettingsMismatchAlert />
19311933
<SettingsSection title="General">
19321934
<SettingsRow
19331935
{...searchableSetting("project-grouping")}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { TriangleAlertIcon } from "lucide-react";
2+
3+
import { useSharedSettingsSync } from "../../hooks/useSettings";
4+
import { Alert, AlertAction, AlertDescription } from "../ui/alert";
5+
import { Button } from "../ui/button";
6+
7+
/**
8+
* Warns when a connected environment holds different shared settings than
9+
* the primary one, and offers to write the primary's values everywhere.
10+
* Renders nothing when every connected environment agrees.
11+
*/
12+
export function SharedSettingsMismatchAlert() {
13+
const { mismatches, applyToAll } = useSharedSettingsSync();
14+
if (mismatches.length === 0) {
15+
return null;
16+
}
17+
const labels = mismatches.map((mismatch) => mismatch.label).join(", ");
18+
return (
19+
<Alert variant="warning" className="mx-3 sm:mx-4">
20+
<TriangleAlertIcon />
21+
<AlertDescription>
22+
Settings differ on {labels}. Thread and source control preferences are meant to match on
23+
every environment.
24+
</AlertDescription>
25+
<AlertAction>
26+
<Button variant="outline" size="compact" onClick={applyToAll}>
27+
Apply to all
28+
</Button>
29+
</AlertAction>
30+
</Alert>
31+
);
32+
}

‎apps/web/src/components/settings/SourceControlSettings.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "@t3tools/shared/backgroundActivitySettings";
1919

2020
import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";
21+
import { SharedSettingsMismatchAlert } from "./SharedSettingsMismatchAlert";
2122
import { cn } from "../../lib/utils";
2223
import { useEnvironments, usePrimaryEnvironment } from "../../state/environments";
2324
import { useEnvironmentQuery } from "../../state/query";
@@ -548,6 +549,7 @@ export function SourceControlSettingsPanel() {
548549

549550
return (
550551
<SettingsPageContainer>
552+
<SharedSettingsMismatchAlert />
551553
{isInitialScanPending ? (
552554
<>
553555
<SourceControlSectionSkeleton title="Version Control" headerAction={scanButton} />

0 commit comments

Comments
 (0)