@@ -19,7 +19,7 @@ import {
1919 squashAtomCommandFailure ,
2020} from "@t3tools/client-runtime/state/runtime" ;
2121import { AndroidScreenHeader } from "../../components/AndroidScreenHeader" ;
22- import { AppText as Text } from "../../components/AppText" ;
22+ import { AppText as Text , AppTextInput as TextInput } from "../../components/AppText" ;
2323import { supportsAgentAwarenessPush } from "../agent-awareness/capabilities" ;
2424import { setLiveActivityUpdatesEnabled } from "../agent-awareness/liveActivityPreferences" ;
2525import { requestAgentNotificationPermission } from "../agent-awareness/notificationPermissions" ;
@@ -36,7 +36,17 @@ import { runtime } from "../../lib/runtime";
3636import { mobilePreferencesAtom , updateMobilePreferencesAtom } from "../../state/preferences" ;
3737import { serverEnvironment } from "../../state/server" ;
3838import { 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" ;
4050import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled" ;
4151import {
4252 type AppUpdateCheckState ,
@@ -530,51 +540,136 @@ function ConfiguredSettingsRouteScreen() {
530540}
531541
532542function 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
0 commit comments