| author | |
| committer | |
| log | 38154388d7b0274aefd3f29c38db3b2659c46c29 |
| tree | 7622c4283ba5c8494160d8e5b08ce99d2e98bed4 |
| parent | 22c311ddecfbab2e541a374a46f2df87d4fc6305 |
| signature | Signed by PGP key B5690EEEBB952194 |
25 files changed, 231 insertions(+), 568 deletions(-)
apps/desktop/src/settings/DesktopClientSettings.test.ts+1-1| ... | @@ -40,7 +40,7 @@ const clientSettings: ClientSettings = { | ... | @@ -40,7 +40,7 @@ const clientSettings: ClientSettings = { |
| 40 | showSkillsInSlashMenu: false, | 40 | showSkillsInSlashMenu: false, |
| 41 | providerModelPreferences: {}, | 41 | providerModelPreferences: {}, |
| 42 | sidebarAutoSettleAfterDays: 3, | 42 | sidebarAutoSettleAfterDays: 3, |
| 43 | sidebarAutoSettleMode: "change-request", | 43 | sidebarAutoSettleOnMerge: true, |
| 44 | sidebarProjectGroupingMode: "repository_path", | 44 | sidebarProjectGroupingMode: "repository_path", |
| 45 | sidebarProjectGroupingOverrides: { | 45 | sidebarProjectGroupingOverrides: { |
| 46 | "environment-1:/tmp/project-a": "separate", | 46 | "environment-1:/tmp/project-a": "separate", |
apps/mobile/src/Stack.tsx-8| ... | @@ -51,7 +51,6 @@ import { | ... | @@ -51,7 +51,6 @@ import { |
| 51 | import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider"; | 51 | import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider"; |
| 52 | import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen"; | 52 | import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen"; |
| 53 | import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen"; | 53 | import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen"; |
| 54 | import { SettingsAutoSettleRouteScreen } from "./features/settings/SettingsAutoSettleRouteScreen"; | ||
| 55 | import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen"; | 54 | import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen"; |
| 56 | import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; | 55 | import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; |
| 57 | import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; | 56 | import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; |
| ... | @@ -178,13 +177,6 @@ const SettingsContentStack = createNativeStackNavigator({ | ... | @@ -178,13 +177,6 @@ const SettingsContentStack = createNativeStackNavigator({ |
| 178 | title: "Appearance", | 177 | title: "Appearance", |
| 179 | }, | 178 | }, |
| 180 | }), | 179 | }), |
| 181 | SettingsAutoSettle: createNativeStackScreen({ | ||
| 182 | screen: SettingsAutoSettleRouteScreen, | ||
| 183 | linking: "thread-settling", | ||
| 184 | options: { | ||
| 185 | title: "Thread Settling", | ||
| 186 | }, | ||
| 187 | }), | ||
| 188 | SettingsProjectGrouping: createNativeStackScreen({ | 180 | SettingsProjectGrouping: createNativeStackScreen({ |
| 189 | screen: SettingsProjectGroupingRouteScreen, | 181 | screen: SettingsProjectGroupingRouteScreen, |
| 190 | linking: "project-grouping", | 182 | linking: "project-grouping", |
apps/mobile/src/features/home/HomeScreen.tsx+5-8| ... | @@ -30,7 +30,6 @@ import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspace | ... | @@ -30,7 +30,6 @@ import type { WorkspaceEnvironment, WorkspaceState } from "../../state/workspace |
| 30 | import type { SavedRemoteConnection } from "../../lib/connection"; | 30 | import type { SavedRemoteConnection } from "../../lib/connection"; |
| 31 | import { scopedProjectKey } from "../../lib/scopedEntities"; | 31 | import { scopedProjectKey } from "../../lib/scopedEntities"; |
| 32 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | 32 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 33 | import { resolveMobileAutoSettlePreferences } from "../../persistence/mobile-preferences"; | ||
| 34 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; | 33 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; |
| 35 | import { useThreadSearch } from "../../state/queries"; | 34 | import { useThreadSearch } from "../../state/queries"; |
| 36 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; | 35 | import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled"; |
| ... | @@ -209,9 +208,9 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -209,9 +208,9 @@ export function HomeScreen(props: HomeScreenProps) { |
| 209 | >(() => new Map()); | 208 | >(() => new Map()); |
| 210 | const preferencesResult = useAtomValue(mobilePreferencesAtom); | 209 | const preferencesResult = useAtomValue(mobilePreferencesAtom); |
| 211 | const threadListV2Enabled = useThreadListV2Enabled(); | 210 | const threadListV2Enabled = useThreadListV2Enabled(); |
| 212 | const autoSettlePreferences = resolveMobileAutoSettlePreferences( | 211 | const autoSettleOnMerge = |
| 213 | AsyncResult.isSuccess(preferencesResult) ? preferencesResult.value : {}, | 212 | !AsyncResult.isSuccess(preferencesResult) || |
| 214 | ); | 213 | preferencesResult.value.autoSettleOnMerge !== false; |
| 215 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); | 214 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); |
| 216 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); | 215 | const openSwipeableRef = useRef<SwipeableMethods | null>(null); |
| 217 | const listRef = useRef<LegendListRef | null>(null); | 216 | const listRef = useRef<LegendListRef | null>(null); |
| ... | @@ -679,8 +678,7 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -679,8 +678,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 679 | searchQuery: props.searchQuery, | 678 | searchQuery: props.searchQuery, |
| 680 | matchedThreadKeys, | 679 | matchedThreadKeys, |
| 681 | changeRequestByKey, | 680 | changeRequestByKey, |
| 682 | autoSettleAfterDays: autoSettlePreferences.autoSettleAfterDays, | 681 | autoSettleOnMerge, |
| 683 | autoSettleMode: autoSettlePreferences.autoSettleMode, | ||
| 684 | settlementEnvironmentIds, | 682 | settlementEnvironmentIds, |
| 685 | snoozeEnvironmentIds, | 683 | snoozeEnvironmentIds, |
| 686 | settledLimit: settledVisibleCount, | 684 | settledLimit: settledVisibleCount, |
| ... | @@ -692,8 +690,7 @@ export function HomeScreen(props: HomeScreenProps) { | ... | @@ -692,8 +690,7 @@ export function HomeScreen(props: HomeScreenProps) { |
| 692 | }); | 690 | }); |
| 693 | }, [ | 691 | }, [ |
| 694 | changeRequestByKey, | 692 | changeRequestByKey, |
| 695 | autoSettlePreferences.autoSettleAfterDays, | 693 | autoSettleOnMerge, |
| 696 | autoSettlePreferences.autoSettleMode, | ||
| 697 | nowMinute, | 694 | nowMinute, |
| 698 | snoozeWakeTick, | 695 | snoozeWakeTick, |
| 699 | snoozedShelfExpanded, | 696 | snoozedShelfExpanded, |
apps/mobile/src/features/settings/SettingsAutoSettleRouteScreen.tsx deleted-173| ... | @@ -1,173 +0,0 @@ | ||
| 1 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | ||
| 2 | import { useNavigation } from "@react-navigation/native"; | ||
| 3 | import { | ||
| 4 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 5 | MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 6 | type SidebarAutoSettleMode, | ||
| 7 | } from "@t3tools/contracts"; | ||
| 8 | import { AsyncResult } from "effect/unstable/reactivity"; | ||
| 9 | import { Platform, Pressable, ScrollView, View } from "react-native"; | ||
| 10 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | ||
| 11 | |||
| 12 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | ||
| 13 | import { AppText as Text } from "../../components/AppText"; | ||
| 14 | import { SymbolView } from "../../components/AppSymbol"; | ||
| 15 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | ||
| 16 | import { NativeStackScreenOptions } from "../../native/StackHeader"; | ||
| 17 | import { resolveMobileAutoSettlePreferences } from "../../persistence/mobile-preferences"; | ||
| 18 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; | ||
| 19 | import { SettingsSection } from "./components/SettingsSection"; | ||
| 20 | |||
| 21 | const AUTO_SETTLE_OPTIONS: ReadonlyArray<{ | ||
| 22 | readonly mode: SidebarAutoSettleMode; | ||
| 23 | readonly label: string; | ||
| 24 | readonly description: string; | ||
| 25 | }> = [ | ||
| 26 | { | ||
| 27 | mode: "never", | ||
| 28 | label: "Never", | ||
| 29 | description: "Threads settle only when you choose Settle.", | ||
| 30 | }, | ||
| 31 | { | ||
| 32 | mode: "change-request", | ||
| 33 | label: "When a pull request merges or closes", | ||
| 34 | description: "A missing or uncertain pull-request timestamp keeps the thread active.", | ||
| 35 | }, | ||
| 36 | { | ||
| 37 | mode: "inactivity", | ||
| 38 | label: "After inactivity", | ||
| 39 | description: "Running, blocked, and open-pull-request threads stay active.", | ||
| 40 | }, | ||
| 41 | ]; | ||
| 42 | |||
| 43 | export function mobileAutoSettleModeLabel(mode: SidebarAutoSettleMode): string { | ||
| 44 | if (mode === "change-request") return "PR merged or closed"; | ||
| 45 | if (mode === "inactivity") return "After inactivity"; | ||
| 46 | return "Never"; | ||
| 47 | } | ||
| 48 | |||
| 49 | export function SettingsAutoSettleRouteScreen() { | ||
| 50 | const navigation = useNavigation(); | ||
| 51 | const insets = useSafeAreaInsets(); | ||
| 52 | const checkmarkColor = useUniwindTheme()["--color-icon"]; | ||
| 53 | const preferencesResult = useAtomValue(mobilePreferencesAtom); | ||
| 54 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); | ||
| 55 | const preferencesReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting; | ||
| 56 | const resolved = resolveMobileAutoSettlePreferences( | ||
| 57 | AsyncResult.isSuccess(preferencesResult) ? preferencesResult.value : {}, | ||
| 58 | ); | ||
| 59 | |||
| 60 | const setDays = (days: number) => { | ||
| 61 | if (!preferencesReady) return; | ||
| 62 | savePreferences({ | ||
| 63 | autoSettleAfterDays: Math.min( | ||
| 64 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 65 | Math.max(MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, days), | ||
| 66 | ), | ||
| 67 | }); | ||
| 68 | }; | ||
| 69 | |||
| 70 | return ( | ||
| 71 | <View collapsable={false} className="flex-1 bg-sheet"> | ||
| 72 | {Platform.OS === "android" ? ( | ||
| 73 | <> | ||
| 74 | <NativeStackScreenOptions options={{ headerShown: false }} /> | ||
| 75 | <AndroidScreenHeader title="Thread Settling" onBack={() => navigation.goBack()} /> | ||
| 76 | </> | ||
| 77 | ) : null} | ||
| 78 | <ScrollView | ||
| 79 | contentInsetAdjustmentBehavior="automatic" | ||
| 80 | showsVerticalScrollIndicator={false} | ||
| 81 | className="flex-1" | ||
| 82 | contentContainerClassName="gap-4 px-5 pt-4" | ||
| 83 | contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }} | ||
| 84 | > | ||
| 85 | <SettingsSection title="Automatic settling"> | ||
| 86 | {AUTO_SETTLE_OPTIONS.map((option, index) => ( | ||
| 87 | <Pressable | ||
| 88 | key={option.mode} | ||
| 89 | accessibilityRole="radio" | ||
| 90 | accessibilityState={{ | ||
| 91 | checked: resolved.autoSettleMode === option.mode, | ||
| 92 | disabled: !preferencesReady, | ||
| 93 | }} | ||
| 94 | disabled={!preferencesReady} | ||
| 95 | onPress={() => savePreferences({ autoSettleMode: option.mode })} | ||
| 96 | className={ | ||
| 97 | index === 0 | ||
| 98 | ? "flex-row items-center gap-4 p-4" | ||
| 99 | : "flex-row items-center gap-4 border-t border-border-subtle p-4" | ||
| 100 | } | ||
| 101 | > | ||
| 102 | <View className="min-w-0 flex-1 gap-1"> | ||
| 103 | <Text className="text-lg text-foreground">{option.label}</Text> | ||
| 104 | <Text className="text-sm leading-normal text-foreground-muted"> | ||
| 105 | {option.description} | ||
| 106 | </Text> | ||
| 107 | </View> | ||
| 108 | {resolved.autoSettleMode === option.mode ? ( | ||
| 109 | <SymbolView | ||
| 110 | name="checkmark" | ||
| 111 | size={18} | ||
| 112 | tintColor={checkmarkColor} | ||
| 113 | type="monochrome" | ||
| 114 | weight="semibold" | ||
| 115 | /> | ||
| 116 | ) : null} | ||
| 117 | </Pressable> | ||
| 118 | ))} | ||
| 119 | </SettingsSection> | ||
| 120 | |||
| 121 | {resolved.autoSettleMode === "inactivity" ? ( | ||
| 122 | <SettingsSection title="Inactivity window"> | ||
| 123 | <View className="flex-row items-center gap-4 p-4"> | ||
| 124 | <View className="min-w-0 flex-1 gap-1"> | ||
| 125 | <Text className="text-lg text-foreground">Days without activity</Text> | ||
| 126 | <Text className="text-sm text-foreground-muted">Between 1 and 90 days</Text> | ||
| 127 | </View> | ||
| 128 | <Pressable | ||
| 129 | accessibilityLabel="Decrease inactivity days" | ||
| 130 | accessibilityRole="button" | ||
| 131 | disabled={ | ||
| 132 | !preferencesReady || | ||
| 133 | resolved.autoSettleAfterDays <= MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS | ||
| 134 | } | ||
| 135 | onPress={() => setDays(resolved.autoSettleAfterDays - 1)} | ||
| 136 | className="size-10 items-center justify-center rounded-full bg-fill-secondary disabled:opacity-40" | ||
| 137 | > | ||
| 138 | <SymbolView | ||
| 139 | name="minus" | ||
| 140 | size={16} | ||
| 141 | tintColor={checkmarkColor} | ||
| 142 | type="monochrome" | ||
| 143 | weight="semibold" | ||
| 144 | /> | ||
| 145 | </Pressable> | ||
| 146 | <Text className="w-8 text-center text-lg text-foreground"> | ||
| 147 | {resolved.autoSettleAfterDays} | ||
| 148 | </Text> | ||
| 149 | <Pressable | ||
| 150 | accessibilityLabel="Increase inactivity days" | ||
| 151 | accessibilityRole="button" | ||
| 152 | disabled={ | ||
| 153 | !preferencesReady || | ||
| 154 | resolved.autoSettleAfterDays >= MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS | ||
| 155 | } | ||
| 156 | onPress={() => setDays(resolved.autoSettleAfterDays + 1)} | ||
| 157 | className="size-10 items-center justify-center rounded-full bg-fill-secondary disabled:opacity-40" | ||
| 158 | > | ||
| 159 | <SymbolView | ||
| 160 | name="plus" | ||
| 161 | size={16} | ||
| 162 | tintColor={checkmarkColor} | ||
| 163 | type="monochrome" | ||
| 164 | weight="semibold" | ||
| 165 | /> | ||
| 166 | </Pressable> | ||
| 167 | </View> | ||
| 168 | </SettingsSection> | ||
| 169 | ) : null} | ||
| 170 | </ScrollView> | ||
| 171 | </View> | ||
| 172 | ); | ||
| 173 | } | ||
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+8-9| ... | @@ -4,7 +4,6 @@ import Constants from "expo-constants"; | ... | @@ -4,7 +4,6 @@ import Constants from "expo-constants"; |
| 4 | import * as Notifications from "expo-notifications"; | 4 | import * as Notifications from "expo-notifications"; |
| 5 | import { useNavigation } from "@react-navigation/native"; | 5 | import { useNavigation } from "@react-navigation/native"; |
| 6 | import { NativeStackScreenOptions } from "../../native/StackHeader"; | 6 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 7 | import { resolveMobileAutoSettlePreferences } from "../../persistence/mobile-preferences"; | ||
| 8 | import { SymbolView } from "../../components/AppSymbol"; | 7 | import { SymbolView } from "../../components/AppSymbol"; |
| 9 | import * as Effect from "effect/Effect"; | 8 | import * as Effect from "effect/Effect"; |
| 10 | import { AsyncResult } from "effect/unstable/reactivity"; | 9 | import { AsyncResult } from "effect/unstable/reactivity"; |
| ... | @@ -46,7 +45,6 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re | ... | @@ -46,7 +45,6 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re |
| 46 | import { SettingsRow } from "./components/SettingsRow"; | 45 | import { SettingsRow } from "./components/SettingsRow"; |
| 47 | import { SettingsSection } from "./components/SettingsSection"; | 46 | import { SettingsSection } from "./components/SettingsSection"; |
| 48 | import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; | 47 | import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; |
| 49 | import { mobileAutoSettleModeLabel } from "./SettingsAutoSettleRouteScreen"; | ||
| 50 | import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic"; | 48 | import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic"; |
| 51 | 49 | ||
| 52 | type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; | 50 | type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; |
| ... | @@ -530,18 +528,19 @@ function ConfiguredSettingsRouteScreen() { | ... | @@ -530,18 +528,19 @@ function ConfiguredSettingsRouteScreen() { |
| 530 | 528 | ||
| 531 | function GeneralSettingsSection() { | 529 | function GeneralSettingsSection() { |
| 532 | const preferencesResult = useAtomValue(mobilePreferencesAtom); | 530 | const preferencesResult = useAtomValue(mobilePreferencesAtom); |
| 533 | const autoSettlePreferences = resolveMobileAutoSettlePreferences( | 531 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); |
| 534 | AsyncResult.isSuccess(preferencesResult) ? preferencesResult.value : {}, | 532 | const autoSettleOnMerge = |
| 535 | ); | 533 | !AsyncResult.isSuccess(preferencesResult) || |
| 534 | preferencesResult.value.autoSettleOnMerge !== false; | ||
| 536 | 535 | ||
| 537 | return ( | 536 | return ( |
| 538 | <SettingsSection title="General"> | 537 | <SettingsSection title="General"> |
| 539 | <SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" /> | 538 | <SettingsRow icon="folder" label="Project Grouping" target="SettingsProjectGrouping" /> |
| 540 | <SettingsRow | 539 | <SettingsSwitchRow |
| 541 | icon="arrow.triangle.branch" | 540 | icon="arrow.triangle.branch" |
| 542 | label="Thread Settling" | 541 | label="Auto-settle merged threads" |
| 543 | value={mobileAutoSettleModeLabel(autoSettlePreferences.autoSettleMode)} | 542 | value={autoSettleOnMerge} |
| 544 | target="SettingsAutoSettle" | 543 | onValueChange={(value) => savePreferences({ autoSettleOnMerge: value })} |
| 545 | /> | 544 | /> |
| 546 | <SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" /> | 545 | <SettingsRow icon="chart.bar.xaxis" label="Usage" target="SettingsUsage" /> |
| 547 | </SettingsSection> | 546 | </SettingsSection> |
apps/mobile/src/features/settings/components/settings-sheet-targets.ts-1| ... | @@ -2,7 +2,6 @@ export type SettingsSheetTarget = | ... | @@ -2,7 +2,6 @@ export type SettingsSheetTarget = |
| 2 | | "SettingsEnvironments" | 2 | | "SettingsEnvironments" |
| 3 | | "SettingsArchive" | 3 | | "SettingsArchive" |
| 4 | | "SettingsAppearance" | 4 | | "SettingsAppearance" |
| 5 | | "SettingsAutoSettle" | ||
| 6 | | "SettingsProjectGrouping" | 5 | | "SettingsProjectGrouping" |
| 7 | | "SettingsClientStorage" | 6 | | "SettingsClientStorage" |
| 8 | | "SettingsUsage"; | 7 | | "SettingsUsage"; |
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+5-8| ... | @@ -26,7 +26,6 @@ import { ControlPillMenu } from "../../components/ControlPill"; | ... | @@ -26,7 +26,6 @@ import { ControlPillMenu } from "../../components/ControlPill"; |
| 26 | import { SymbolView } from "../../components/AppSymbol"; | 26 | import { SymbolView } from "../../components/AppSymbol"; |
| 27 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | 27 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 28 | import { NativeStackScreenOptions } from "../../native/StackHeader"; | 28 | import { NativeStackScreenOptions } from "../../native/StackHeader"; |
| 29 | import { resolveMobileAutoSettlePreferences } from "../../persistence/mobile-preferences"; | ||
| 30 | import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; | 29 | import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; |
| 31 | import { useProjects, useThreadShells } from "../../state/entities"; | 30 | import { useProjects, useThreadShells } from "../../state/entities"; |
| 32 | import { mobilePreferencesAtom } from "../../state/preferences"; | 31 | import { mobilePreferencesAtom } from "../../state/preferences"; |
| ... | @@ -166,9 +165,9 @@ function ThreadNavigationSidebarPane( | ... | @@ -166,9 +165,9 @@ function ThreadNavigationSidebarPane( |
| 166 | } = useThreadListActions(); | 165 | } = useThreadListActions(); |
| 167 | const threadListV2Enabled = useThreadListV2Enabled(); | 166 | const threadListV2Enabled = useThreadListV2Enabled(); |
| 168 | const preferencesResult = useAtomValue(mobilePreferencesAtom); | 167 | const preferencesResult = useAtomValue(mobilePreferencesAtom); |
| 169 | const autoSettlePreferences = resolveMobileAutoSettlePreferences( | 168 | const autoSettleOnMerge = |
| 170 | AsyncResult.isSuccess(preferencesResult) ? preferencesResult.value : {}, | 169 | !AsyncResult.isSuccess(preferencesResult) || |
| 171 | ); | 170 | preferencesResult.value.autoSettleOnMerge !== false; |
| 172 | const pendingTasks = usePendingNewTasks(); | 171 | const pendingTasks = usePendingNewTasks(); |
| 173 | const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); | 172 | const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); |
| 174 | const environments = useMemo( | 173 | const environments = useMemo( |
| ... | @@ -511,8 +510,7 @@ function ThreadNavigationSidebarPane( | ... | @@ -511,8 +510,7 @@ function ThreadNavigationSidebarPane( |
| 511 | searchQuery: props.searchQuery, | 510 | searchQuery: props.searchQuery, |
| 512 | matchedThreadKeys, | 511 | matchedThreadKeys, |
| 513 | changeRequestByKey, | 512 | changeRequestByKey, |
| 514 | autoSettleAfterDays: autoSettlePreferences.autoSettleAfterDays, | 513 | autoSettleOnMerge, |
| 515 | autoSettleMode: autoSettlePreferences.autoSettleMode, | ||
| 516 | settlementEnvironmentIds, | 514 | settlementEnvironmentIds, |
| 517 | snoozeEnvironmentIds, | 515 | snoozeEnvironmentIds, |
| 518 | settledLimit: settledVisibleCount, | 516 | settledLimit: settledVisibleCount, |
| ... | @@ -524,8 +522,7 @@ function ThreadNavigationSidebarPane( | ... | @@ -524,8 +522,7 @@ function ThreadNavigationSidebarPane( |
| 524 | }); | 522 | }); |
| 525 | }, [ | 523 | }, [ |
| 526 | changeRequestByKey, | 524 | changeRequestByKey, |
| 527 | autoSettlePreferences.autoSettleAfterDays, | 525 | autoSettleOnMerge, |
| 528 | autoSettlePreferences.autoSettleMode, | ||
| 529 | nowMinute, | 526 | nowMinute, |
| 530 | snoozeWakeTick, | 527 | snoozeWakeTick, |
| 531 | snoozedShelfExpanded, | 528 | snoozedShelfExpanded, |
apps/mobile/src/features/threads/threadListV2.test.ts+7-15| ... | @@ -360,12 +360,10 @@ describe("buildThreadListV2Items", () => { | ... | @@ -360,12 +360,10 @@ describe("buildThreadListV2Items", () => { |
| 360 | `${environmentId}:${thread.id}`, | 360 | `${environmentId}:${thread.id}`, |
| 361 | { | 361 | { |
| 362 | state: "merged" as const, | 362 | state: "merged" as const, |
| 363 | updatedAt: NOW, | ||
| 364 | linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]', | 363 | linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]', |
| 365 | }, | 364 | }, |
| 366 | ], | 365 | ], |
| 367 | ]), | 366 | ]), |
| 368 | autoSettleMode: "change-request", | ||
| 369 | now: NOW, | 367 | now: NOW, |
| 370 | }); | 368 | }); |
| 371 | 369 | ||
| ... | @@ -373,16 +371,16 @@ describe("buildThreadListV2Items", () => { | ... | @@ -373,16 +371,16 @@ describe("buildThreadListV2Items", () => { |
| 373 | expect(layout.items[0]?.variant).toBe("slim"); | 371 | expect(layout.items[0]?.variant).toBe("slim"); |
| 374 | }); | 372 | }); |
| 375 | 373 | ||
| 376 | it("keeps a merged thread active when automatic settling is off", () => { | 374 | it("keeps a merged thread active when auto-settle on merge is off", () => { |
| 377 | const merged = makeThread({ id: ThreadId.make("merged"), title: "Merged" }); | 375 | const merged = makeThread({ id: ThreadId.make("merged"), title: "Merged" }); |
| 378 | const layout = buildThreadListV2Items({ | 376 | const layout = buildThreadListV2Items({ |
| 379 | threads: [merged], | 377 | threads: [merged], |
| 380 | environmentId: null, | 378 | environmentId: null, |
| 381 | searchQuery: "", | 379 | searchQuery: "", |
| 382 | changeRequestByKey: new Map([ | 380 | changeRequestByKey: new Map([ |
| 383 | [`${environmentId}:${merged.id}`, { state: "merged" as const, updatedAt: NOW }], | 381 | [`${environmentId}:${merged.id}`, { state: "merged" as const }], |
| 384 | ]), | 382 | ]), |
| 385 | autoSettleMode: "never", | 383 | autoSettleOnMerge: false, |
| 386 | now: NOW, | 384 | now: NOW, |
| 387 | }); | 385 | }); |
| 388 | 386 | ||
| ... | @@ -451,10 +449,7 @@ describe("buildThreadListV2Items", () => { | ... | @@ -451,10 +449,7 @@ describe("buildThreadListV2Items", () => { |
| 451 | threads: [makeThread({ id: ThreadId.make("active"), title: "Active" }), merged], | 449 | threads: [makeThread({ id: ThreadId.make("active"), title: "Active" }), merged], |
| 452 | environmentId: null, | 450 | environmentId: null, |
| 453 | searchQuery: "", | 451 | searchQuery: "", |
| 454 | changeRequestByKey: new Map([ | 452 | changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]), |
| 455 | [`${environmentId}:${merged.id}`, { state: "merged", updatedAt: NOW }], | ||
| 456 | ]), | ||
| 457 | autoSettleMode: "change-request", | ||
| 458 | now: NOW, | 453 | now: NOW, |
| 459 | }); | 454 | }); |
| 460 | 455 | ||
| ... | @@ -483,7 +478,6 @@ describe("buildThreadListV2Items", () => { | ... | @@ -483,7 +478,6 @@ describe("buildThreadListV2Items", () => { |
| 483 | threads: [inactive], | 478 | threads: [inactive], |
| 484 | environmentId: null, | 479 | environmentId: null, |
| 485 | searchQuery: "", | 480 | searchQuery: "", |
| 486 | autoSettleMode: "inactivity", | ||
| 487 | now: NOW, | 481 | now: NOW, |
| 488 | }); | 482 | }); |
| 489 | 483 | ||
| ... | @@ -495,7 +489,7 @@ describe("buildThreadListV2Items", () => { | ... | @@ -495,7 +489,7 @@ describe("buildThreadListV2Items", () => { |
| 495 | expect(layout.settledCount).toBe(1); | 489 | expect(layout.settledCount).toBe(1); |
| 496 | }); | 490 | }); |
| 497 | 491 | ||
| 498 | it("keeps pinned merged threads pinned when automatic settling is off", () => { | 492 | it("keeps pinned merged threads pinned when auto-settle on merge is off", () => { |
| 499 | const merged = makeThread({ | 493 | const merged = makeThread({ |
| 500 | id: ThreadId.make("pinned-merged"), | 494 | id: ThreadId.make("pinned-merged"), |
| 501 | title: "Pinned merged pull request", | 495 | title: "Pinned merged pull request", |
| ... | @@ -505,10 +499,8 @@ describe("buildThreadListV2Items", () => { | ... | @@ -505,10 +499,8 @@ describe("buildThreadListV2Items", () => { |
| 505 | threads: [merged], | 499 | threads: [merged], |
| 506 | environmentId: null, | 500 | environmentId: null, |
| 507 | searchQuery: "", | 501 | searchQuery: "", |
| 508 | changeRequestByKey: new Map([ | 502 | changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]), |
| 509 | [`${environmentId}:${merged.id}`, { state: "merged", updatedAt: NOW }], | 503 | autoSettleOnMerge: false, |
| 510 | ]), | ||
| 511 | autoSettleMode: "never", | ||
| 512 | now: NOW, | 504 | now: NOW, |
| 513 | }); | 505 | }); |
| 514 | 506 |
apps/mobile/src/features/threads/threadListV2.ts+4-9| ... | @@ -16,12 +16,7 @@ import { | ... | @@ -16,12 +16,7 @@ import { |
| 16 | activeThreadAnchorTimestampMs, | 16 | activeThreadAnchorTimestampMs, |
| 17 | sortPinnedThreadsByOrderKey, | 17 | sortPinnedThreadsByOrderKey, |
| 18 | } from "@t3tools/client-runtime/state/thread-sort"; | 18 | } from "@t3tools/client-runtime/state/thread-sort"; |
| 19 | import type { | 19 | import type { EnvironmentId, ProjectId, ThreadLinkedPullRequest } from "@t3tools/contracts"; |
| 20 | EnvironmentId, | ||
| 21 | ProjectId, | ||
| 22 | SidebarAutoSettleMode, | ||
| 23 | ThreadLinkedPullRequest, | ||
| 24 | } from "@t3tools/contracts"; | ||
| 25 | 20 | ||
| 26 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; | 21 | import type { PendingNewTask } from "../../state/use-pending-new-tasks"; |
| 27 | 22 | ||
| ... | @@ -374,7 +369,7 @@ export function buildThreadListV2Items(input: { | ... | @@ -374,7 +369,7 @@ export function buildThreadListV2Items(input: { |
| 374 | contract as settlementEnvironmentIds. */ | 369 | contract as settlementEnvironmentIds. */ |
| 375 | readonly snoozeEnvironmentIds?: ReadonlySet<EnvironmentId>; | 370 | readonly snoozeEnvironmentIds?: ReadonlySet<EnvironmentId>; |
| 376 | readonly autoSettleAfterDays?: number; | 371 | readonly autoSettleAfterDays?: number; |
| 377 | readonly autoSettleMode?: SidebarAutoSettleMode; | 372 | readonly autoSettleOnMerge?: boolean; |
| 378 | /** Max settled rows to render; the rest are counted, not built. */ | 373 | /** Max settled rows to render; the rest are counted, not built. */ |
| 379 | readonly settledLimit?: number; | 374 | readonly settledLimit?: number; |
| 380 | /** Injectable for tests; defaults to now. */ | 375 | /** Injectable for tests; defaults to now. */ |
| ... | @@ -395,7 +390,7 @@ export function buildThreadListV2Items(input: { | ... | @@ -395,7 +390,7 @@ export function buildThreadListV2Items(input: { |
| 395 | const now = input.now ?? new Date().toISOString(); | 390 | const now = input.now ?? new Date().toISOString(); |
| 396 | const snoozeNow = input.snoozeNow ?? now; | 391 | const snoozeNow = input.snoozeNow ?? now; |
| 397 | const autoSettleAfterDays = input.autoSettleAfterDays ?? 3; | 392 | const autoSettleAfterDays = input.autoSettleAfterDays ?? 3; |
| 398 | const autoSettleMode = input.autoSettleMode ?? "never"; | 393 | const autoSettleOnMerge = input.autoSettleOnMerge ?? true; |
| 399 | const query = input.searchQuery.trim().toLocaleLowerCase(); | 394 | const query = input.searchQuery.trim().toLocaleLowerCase(); |
| 400 | const projectKeys = input.projectRefs | 395 | const projectKeys = input.projectRefs |
| 401 | ? new Set(input.projectRefs.map((ref) => `${ref.environmentId}:${ref.projectId}`)) | 396 | ? new Set(input.projectRefs.map((ref) => `${ref.environmentId}:${ref.projectId}`)) |
| ... | @@ -452,7 +447,7 @@ export function buildThreadListV2Items(input: { | ... | @@ -452,7 +447,7 @@ export function buildThreadListV2Items(input: { |
| 452 | effectiveSettled(thread, { | 447 | effectiveSettled(thread, { |
| 453 | now, | 448 | now, |
| 454 | autoSettleAfterDays, | 449 | autoSettleAfterDays, |
| 455 | autoSettleMode, | 450 | autoSettleOnMerge, |
| 456 | changeRequest, | 451 | changeRequest, |
| 457 | }) | 452 | }) |
| 458 | ) { | 453 | ) { |
apps/mobile/src/lib/storage.test.ts-29| ... | @@ -100,7 +100,6 @@ import { | ... | @@ -100,7 +100,6 @@ import { |
| 100 | saveConnection, | 100 | saveConnection, |
| 101 | savePreferencesPatch, | 101 | savePreferencesPatch, |
| 102 | } from "../persistence/imperative"; | 102 | } from "../persistence/imperative"; |
| 103 | import { resolveMobileAutoSettlePreferences } from "../persistence/mobile-preferences"; | ||
| 104 | import { toStableSavedRemoteConnection } from "./connection"; | 103 | import { toStableSavedRemoteConnection } from "./connection"; |
| 105 | 104 | ||
| 106 | const managedConnection = { | 105 | const managedConnection = { |
| ... | @@ -203,34 +202,6 @@ describe("mobile connection storage", () => { | ... | @@ -203,34 +202,6 @@ describe("mobile connection storage", () => { |
| 203 | await expect(loadPreferences()).resolves.toEqual({}); | 202 | await expect(loadPreferences()).resolves.toEqual({}); |
| 204 | }); | 203 | }); |
| 205 | 204 | ||
| 206 | it("defaults retired mobile settling preferences to manual-only", async () => { | ||
| 207 | mocks.setPreferencesJson(JSON.stringify({ autoSettleOnMerge: true }), 10); | ||
| 208 | |||
| 209 | const preferences = await loadPreferences(); | ||
| 210 | expect(preferences).toEqual({}); | ||
| 211 | expect(resolveMobileAutoSettlePreferences(preferences)).toEqual({ | ||
| 212 | autoSettleMode: "never", | ||
| 213 | autoSettleAfterDays: 3, | ||
| 214 | }); | ||
| 215 | }); | ||
| 216 | |||
| 217 | it("preserves valid mobile settling preferences", async () => { | ||
| 218 | mocks.setPreferencesJson( | ||
| 219 | JSON.stringify({ autoSettleMode: "inactivity", autoSettleAfterDays: 30 }), | ||
| 220 | 10, | ||
| 221 | ); | ||
| 222 | |||
| 223 | const preferences = await loadPreferences(); | ||
| 224 | expect(preferences).toEqual({ | ||
| 225 | autoSettleMode: "inactivity", | ||
| 226 | autoSettleAfterDays: 30, | ||
| 227 | }); | ||
| 228 | expect(resolveMobileAutoSettlePreferences(preferences)).toEqual({ | ||
| 229 | autoSettleMode: "inactivity", | ||
| 230 | autoSettleAfterDays: 30, | ||
| 231 | }); | ||
| 232 | }); | ||
| 233 | |||
| 234 | it("falls back to secure storage when SQLite cannot save preferences", async () => { | 205 | it("falls back to secure storage when SQLite cannot save preferences", async () => { |
| 235 | mocks.setDatabaseFailures(true, true); | 206 | mocks.setDatabaseFailures(true, true); |
| 236 | await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 }); | 207 | await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 }); |
apps/mobile/src/persistence/mobile-preferences.ts+5-37| ... | @@ -5,15 +5,7 @@ import * as Option from "effect/Option"; | ... | @@ -5,15 +5,7 @@ import * as Option from "effect/Option"; |
| 5 | import * as Ref from "effect/Ref"; | 5 | import * as Ref from "effect/Ref"; |
| 6 | import * as Schema from "effect/Schema"; | 6 | import * as Schema from "effect/Schema"; |
| 7 | import * as Semaphore from "effect/Semaphore"; | 7 | import * as Semaphore from "effect/Semaphore"; |
| 8 | import { | 8 | import type { SidebarProjectGroupingMode } from "@t3tools/contracts"; |
| 9 | DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 10 | DEFAULT_SIDEBAR_AUTO_SETTLE_MODE, | ||
| 11 | MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 12 | MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 13 | type SidebarAutoSettleAfterDays, | ||
| 14 | type SidebarAutoSettleMode, | ||
| 15 | type SidebarProjectGroupingMode, | ||
| 16 | } from "@t3tools/contracts"; | ||
| 17 | import { MOBILE_THEME_IDS, type MobileThemeId, type MobileThemeMode } from "../lib/mobileTheme"; | 9 | import { MOBILE_THEME_IDS, type MobileThemeId, type MobileThemeMode } from "../lib/mobileTheme"; |
| 18 | 10 | ||
| 19 | import * as MobileDatabase from "./mobile-database"; | 11 | import * as MobileDatabase from "./mobile-database"; |
| ... | @@ -39,8 +31,7 @@ export interface Preferences { | ... | @@ -39,8 +31,7 @@ export interface Preferences { |
| 39 | /** @deprecated Kept temporarily so older OTA bundles retain the selected mode. */ | 31 | /** @deprecated Kept temporarily so older OTA bundles retain the selected mode. */ |
| 40 | readonly projectGroupingEnabled?: boolean; | 32 | readonly projectGroupingEnabled?: boolean; |
| 41 | readonly projectGroupingMode?: SidebarProjectGroupingMode; | 33 | readonly projectGroupingMode?: SidebarProjectGroupingMode; |
| 42 | readonly autoSettleMode?: SidebarAutoSettleMode; | 34 | readonly autoSettleOnMerge?: boolean; |
| 43 | readonly autoSettleAfterDays?: SidebarAutoSettleAfterDays; | ||
| 44 | /** | 35 | /** |
| 45 | * Device-local mirror of the web `legacySidebarEnabled` setting. Mobile has | 36 | * Device-local mirror of the web `legacySidebarEnabled` setting. Mobile has |
| 46 | * no client-settings sync, so the legacy grouped thread list is opted into | 37 | * no client-settings sync, so the legacy grouped thread list is opted into |
| ... | @@ -110,8 +101,7 @@ function sanitizePreferences(parsed: Preferences): Preferences { | ... | @@ -110,8 +101,7 @@ function sanitizePreferences(parsed: Preferences): Preferences { |
| 110 | collapsedProjectGroups?: readonly string[]; | 101 | collapsedProjectGroups?: readonly string[]; |
| 111 | projectGroupingEnabled?: boolean; | 102 | projectGroupingEnabled?: boolean; |
| 112 | projectGroupingMode?: SidebarProjectGroupingMode; | 103 | projectGroupingMode?: SidebarProjectGroupingMode; |
| 113 | autoSettleMode?: SidebarAutoSettleMode; | 104 | autoSettleOnMerge?: boolean; |
| 114 | autoSettleAfterDays?: SidebarAutoSettleAfterDays; | ||
| 115 | legacyThreadListEnabled?: boolean; | 105 | legacyThreadListEnabled?: boolean; |
| 116 | planModeEnabled?: boolean; | 106 | planModeEnabled?: boolean; |
| 117 | threadListV2SettledShelfExpanded?: boolean; | 107 | threadListV2SettledShelfExpanded?: boolean; |
| ... | @@ -177,20 +167,8 @@ function sanitizePreferences(parsed: Preferences): Preferences { | ... | @@ -177,20 +167,8 @@ function sanitizePreferences(parsed: Preferences): Preferences { |
| 177 | ) { | 167 | ) { |
| 178 | preferences.projectGroupingMode = parsed.projectGroupingMode; | 168 | preferences.projectGroupingMode = parsed.projectGroupingMode; |
| 179 | } | 169 | } |
| 180 | if ( | 170 | if (typeof parsed.autoSettleOnMerge === "boolean") { |
| 181 | parsed.autoSettleMode === "never" || | 171 | preferences.autoSettleOnMerge = parsed.autoSettleOnMerge; |
| 182 | parsed.autoSettleMode === "change-request" || | ||
| 183 | parsed.autoSettleMode === "inactivity" | ||
| 184 | ) { | ||
| 185 | preferences.autoSettleMode = parsed.autoSettleMode; | ||
| 186 | } | ||
| 187 | if ( | ||
| 188 | typeof parsed.autoSettleAfterDays === "number" && | ||
| 189 | Number.isInteger(parsed.autoSettleAfterDays) && | ||
| 190 | parsed.autoSettleAfterDays >= MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS && | ||
| 191 | parsed.autoSettleAfterDays <= MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS | ||
| 192 | ) { | ||
| 193 | preferences.autoSettleAfterDays = parsed.autoSettleAfterDays; | ||
| 194 | } | 172 | } |
| 195 | if (typeof parsed.legacyThreadListEnabled === "boolean") { | 173 | if (typeof parsed.legacyThreadListEnabled === "boolean") { |
| 196 | preferences.legacyThreadListEnabled = parsed.legacyThreadListEnabled; | 174 | preferences.legacyThreadListEnabled = parsed.legacyThreadListEnabled; |
| ... | @@ -207,16 +185,6 @@ function sanitizePreferences(parsed: Preferences): Preferences { | ... | @@ -207,16 +185,6 @@ function sanitizePreferences(parsed: Preferences): Preferences { |
| 207 | return preferences; | 185 | return preferences; |
| 208 | } | 186 | } |
| 209 | 187 | ||
| 210 | export function resolveMobileAutoSettlePreferences(preferences: Preferences): { | ||
| 211 | readonly autoSettleMode: SidebarAutoSettleMode; | ||
| 212 | readonly autoSettleAfterDays: SidebarAutoSettleAfterDays; | ||
| 213 | } { | ||
| 214 | return { | ||
| 215 | autoSettleMode: preferences.autoSettleMode ?? DEFAULT_SIDEBAR_AUTO_SETTLE_MODE, | ||
| 216 | autoSettleAfterDays: preferences.autoSettleAfterDays ?? DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, | ||
| 217 | }; | ||
| 218 | } | ||
| 219 | |||
| 220 | export const make = Effect.fn("MobilePreferencesStore.make")(function* () { | 188 | export const make = Effect.fn("MobilePreferencesStore.make")(function* () { |
| 221 | const database = yield* MobileDatabase.MobileDatabase; | 189 | const database = yield* MobileDatabase.MobileDatabase; |
| 222 | const secureStorage = yield* MobileSecureStorage.MobileSecureStorage; | 190 | const secureStorage = yield* MobileSecureStorage.MobileSecureStorage; |
apps/web/src/components/ChatView.tsx+8-6| ... | @@ -4394,7 +4394,7 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4394,7 +4394,7 @@ function ChatViewContent(props: ChatViewProps) { |
| 4394 | ? activePlan.steps | 4394 | ? activePlan.steps |
| 4395 | : null; | 4395 | : null; |
| 4396 | const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays); | 4396 | const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays); |
| 4397 | const autoSettleMode = useClientSettings((settings) => settings.sidebarAutoSettleMode); | 4397 | const autoSettleOnMerge = useClientSettings((settings) => settings.sidebarAutoSettleOnMerge); |
| 4398 | const linkedPullRequestStatus = useLinkedThreadPullRequest( | 4398 | const linkedPullRequestStatus = useLinkedThreadPullRequest( |
| 4399 | activeThreadRef?.environmentId ?? null, | 4399 | activeThreadRef?.environmentId ?? null, |
| 4400 | linkedThreadPullRequest, | 4400 | linkedThreadPullRequest, |
| ... | @@ -4465,8 +4465,10 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4465,8 +4465,10 @@ function ChatViewContent(props: ChatViewProps) { |
| 4465 | const activeThreadWokeVisible = useMemo(() => { | 4465 | const activeThreadWokeVisible = useMemo(() => { |
| 4466 | if (activeThreadWokeAt === null) return false; | 4466 | if (activeThreadWokeAt === null) return false; |
| 4467 | if ( | 4467 | if ( |
| 4468 | autoSettleMode === "change-request" && | 4468 | changeRequestAutoSettles(activeThreadChangeRequest, { |
| 4469 | changeRequestAutoSettles(activeThreadChangeRequest, { thread: activeThreadShell }) | 4469 | autoSettleOnMerge, |
| 4470 | thread: activeThreadShell, | ||
| 4471 | }) | ||
| 4470 | ) { | 4472 | ) { |
| 4471 | return false; | 4473 | return false; |
| 4472 | } | 4474 | } |
| ... | @@ -4492,21 +4494,21 @@ function ChatViewContent(props: ChatViewProps) { | ... | @@ -4492,21 +4494,21 @@ function ChatViewContent(props: ChatViewProps) { |
| 4492 | activeThreadChangeRequest, | 4494 | activeThreadChangeRequest, |
| 4493 | activeThreadShell, | 4495 | activeThreadShell, |
| 4494 | activeThreadWokeAt, | 4496 | activeThreadWokeAt, |
| 4495 | autoSettleMode, | 4497 | autoSettleOnMerge, |
| 4496 | ]); | 4498 | ]); |
| 4497 | const activeThreadSettled = useMemo(() => { | 4499 | const activeThreadSettled = useMemo(() => { |
| 4498 | if (activeThreadShell === null || !supportsSettlement) return false; | 4500 | if (activeThreadShell === null || !supportsSettlement) return false; |
| 4499 | return effectiveSettled(activeThreadShell, { | 4501 | return effectiveSettled(activeThreadShell, { |
| 4500 | now: `${nowMinute}:00.000Z`, | 4502 | now: `${nowMinute}:00.000Z`, |
| 4501 | autoSettleAfterDays, | 4503 | autoSettleAfterDays, |
| 4502 | autoSettleMode, | 4504 | autoSettleOnMerge, |
| 4503 | changeRequest: activeThreadChangeRequest, | 4505 | changeRequest: activeThreadChangeRequest, |
| 4504 | }); | 4506 | }); |
| 4505 | }, [ | 4507 | }, [ |
| 4506 | activeThreadChangeRequest, | 4508 | activeThreadChangeRequest, |
| 4507 | activeThreadShell, | 4509 | activeThreadShell, |
| 4508 | autoSettleAfterDays, | 4510 | autoSettleAfterDays, |
| 4509 | autoSettleMode, | 4511 | autoSettleOnMerge, |
| 4510 | changeRequestSnapshotByKey, | 4512 | changeRequestSnapshotByKey, |
| 4511 | nowMinute, | 4513 | nowMinute, |
| 4512 | supportsSettlement, | 4514 | supportsSettlement, |
apps/web/src/components/Sidebar.tsx+10-7| ... | @@ -31,7 +31,7 @@ import { | ... | @@ -31,7 +31,7 @@ import { |
| 31 | scopedThreadKey, | 31 | scopedThreadKey, |
| 32 | } from "@t3tools/client-runtime/environment"; | 32 | } from "@t3tools/client-runtime/environment"; |
| 33 | import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts"; | 33 | import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts"; |
| 34 | import type { SidebarAutoSettleMode, TimestampFormat } from "@t3tools/contracts/settings"; | 34 | import type { TimestampFormat } from "@t3tools/contracts/settings"; |
| 35 | import { | 35 | import { |
| 36 | AlarmClockIcon, | 36 | AlarmClockIcon, |
| 37 | AlarmClockOffIcon, | 37 | AlarmClockOffIcon, |
| ... | @@ -714,7 +714,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -714,7 +714,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 714 | // False on environments whose server predates thread.settle/unsettle: | 714 | // False on environments whose server predates thread.settle/unsettle: |
| 715 | // the lifecycle affordances hide entirely rather than fail on click. | 715 | // the lifecycle affordances hide entirely rather than fail on click. |
| 716 | settlementSupported: boolean; | 716 | settlementSupported: boolean; |
| 717 | autoSettleMode: SidebarAutoSettleMode; | 717 | autoSettleOnMerge: boolean; |
| 718 | // Same contract for thread.snooze/unsnooze. | 718 | // Same contract for thread.snooze/unsnooze. |
| 719 | snoozeSupported: boolean; | 719 | snoozeSupported: boolean; |
| 720 | // Pinned threads show the same pin marker in active, settled, and snoozed | 720 | // Pinned threads show the same pin marker in active, settled, and snoozed |
| ... | @@ -839,7 +839,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { | ... | @@ -839,7 +839,10 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 839 | const isWoke = | 839 | const isWoke = |
| 840 | wokeAtDate !== null && | 840 | wokeAtDate !== null && |
| 841 | (lastVisitedDate === null || lastVisitedDate < wokeAtDate) && | 841 | (lastVisitedDate === null || lastVisitedDate < wokeAtDate) && |
| 842 | !(props.autoSettleMode === "change-request" && changeRequestAutoSettles(pr, { thread })); | 842 | !changeRequestAutoSettles(pr, { |
| 843 | autoSettleOnMerge: props.autoSettleOnMerge, | ||
| 844 | thread, | ||
| 845 | }); | ||
| 843 | // In-flight rows (working, or waiting on approval/input) fade as a whole: | 846 | // In-flight rows (working, or waiting on approval/input) fade as a whole: |
| 844 | // there is nothing for the user to do yet, so prominence is reserved for | 847 | // there is nothing for the user to do yet, so prominence is reserved for |
| 845 | // rows that need a human — done (unread), read-but-unsettled, failed, and | 848 | // rows that need a human — done (unread), read-but-unsettled, failed, and |
| ... | @@ -1737,7 +1740,7 @@ export default function Sidebar() { | ... | @@ -1737,7 +1740,7 @@ export default function Sidebar() { |
| 1737 | const { isMobile, setOpenMobile } = useSidebar(); | 1740 | const { isMobile, setOpenMobile } = useSidebar(); |
| 1738 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); | 1741 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 1739 | const autoSettleAfterDays = useClientSettings((s) => s.sidebarAutoSettleAfterDays); | 1742 | const autoSettleAfterDays = useClientSettings((s) => s.sidebarAutoSettleAfterDays); |
| 1740 | const autoSettleMode = useClientSettings((s) => s.sidebarAutoSettleMode); | 1743 | const autoSettleOnMerge = useClientSettings((s) => s.sidebarAutoSettleOnMerge); |
| 1741 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); | 1744 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); |
| 1742 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); | 1745 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); |
| 1743 | const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); | 1746 | const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder); |
| ... | @@ -2122,7 +2125,7 @@ export default function Sidebar() { | ... | @@ -2122,7 +2125,7 @@ export default function Sidebar() { |
| 2122 | effectiveSettled(thread, { | 2125 | effectiveSettled(thread, { |
| 2123 | now, | 2126 | now, |
| 2124 | autoSettleAfterDays, | 2127 | autoSettleAfterDays, |
| 2125 | autoSettleMode, | 2128 | autoSettleOnMerge, |
| 2126 | changeRequest, | 2129 | changeRequest, |
| 2127 | }) | 2130 | }) |
| 2128 | ) { | 2131 | ) { |
| ... | @@ -2161,7 +2164,7 @@ export default function Sidebar() { | ... | @@ -2161,7 +2164,7 @@ export default function Sidebar() { |
| 2161 | }; | 2164 | }; |
| 2162 | }, [ | 2165 | }, [ |
| 2163 | autoSettleAfterDays, | 2166 | autoSettleAfterDays, |
| 2164 | autoSettleMode, | 2167 | autoSettleOnMerge, |
| 2165 | changeRequestSnapshotByKey, | 2168 | changeRequestSnapshotByKey, |
| 2166 | nowMinute, | 2169 | nowMinute, |
| 2167 | scopedProjectKeys, | 2170 | scopedProjectKeys, |
| ... | @@ -3784,7 +3787,7 @@ export default function Sidebar() { | ... | @@ -3784,7 +3787,7 @@ export default function Sidebar() { |
| 3784 | serverConfigs.get(thread.environmentId)?.environment.capabilities | 3787 | serverConfigs.get(thread.environmentId)?.environment.capabilities |
| 3785 | .threadSettlement === true | 3788 | .threadSettlement === true |
| 3786 | } | 3789 | } |
| 3787 | autoSettleMode={autoSettleMode} | 3790 | autoSettleOnMerge={autoSettleOnMerge} |
| 3788 | snoozeSupported={ | 3791 | snoozeSupported={ |
| 3789 | serverConfigs.get(thread.environmentId)?.environment.capabilities | 3792 | serverConfigs.get(thread.environmentId)?.environment.capabilities |
| 3790 | .threadSnooze === true | 3793 | .threadSnooze === true |
apps/web/src/components/ThreadStatusIndicators.test.ts-2| ... | @@ -47,7 +47,6 @@ function mergedFeaturePr(): NonNullable<VcsStatusResult["pr"]> { | ... | @@ -47,7 +47,6 @@ function mergedFeaturePr(): NonNullable<VcsStatusResult["pr"]> { |
| 47 | baseRef: "main", | 47 | baseRef: "main", |
| 48 | headRef: "feature/current", | 48 | headRef: "feature/current", |
| 49 | state: "merged", | 49 | state: "merged", |
| 50 | updatedAt: "2026-04-10T00:00:00.000Z", | ||
| 51 | }; | 50 | }; |
| 52 | } | 51 | } |
| 53 | 52 | ||
| ... | @@ -546,7 +545,6 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { | ... | @@ -546,7 +545,6 @@ describe("resolveDisplayedThreadPr + nextThreadChangeRequestSnapshot", () => { |
| 546 | effectiveSettled(shell, { | 545 | effectiveSettled(shell, { |
| 547 | now: "2026-04-10T00:00:00.000Z", | 546 | now: "2026-04-10T00:00:00.000Z", |
| 548 | autoSettleAfterDays: null, | 547 | autoSettleAfterDays: null, |
| 549 | autoSettleMode: "change-request", | ||
| 550 | changeRequest: displayed, | 548 | changeRequest: displayed, |
| 551 | }), | 549 | }), |
| 552 | ).toBe(true); | 550 | ).toBe(true); |
apps/web/src/components/settings/SettingsPanels.logic.test.ts+1-20| ... | @@ -14,10 +14,9 @@ import { | ... | @@ -14,10 +14,9 @@ import { |
| 14 | formatDiagnosticsDescription, | 14 | formatDiagnosticsDescription, |
| 15 | getChangedBrowserSettingLabels, | 15 | getChangedBrowserSettingLabels, |
| 16 | getChangedTypographySettingLabels, | 16 | getChangedTypographySettingLabels, |
| 17 | isSamePreviewViewport, | ||
| 17 | hasChangedBackgroundActivitySettings, | 18 | hasChangedBackgroundActivitySettings, |
| 18 | hasChangedThreadSettlingSettings, | ||
| 19 | isProjectGroupingEnabled, | 19 | isProjectGroupingEnabled, |
| 20 | isSamePreviewViewport, | ||
| 21 | projectGroupingModeFromToggle, | 20 | projectGroupingModeFromToggle, |
| 22 | resolveBackgroundActivityProfileOption, | 21 | resolveBackgroundActivityProfileOption, |
| 23 | } from "./SettingsPanels.logic"; | 22 | } from "./SettingsPanels.logic"; |
| ... | @@ -153,24 +152,6 @@ describe("project grouping toggle", () => { | ... | @@ -153,24 +152,6 @@ describe("project grouping toggle", () => { |
| 153 | }); | 152 | }); |
| 154 | }); | 153 | }); |
| 155 | 154 | ||
| 156 | describe("thread settling settings restore", () => { | ||
| 157 | it("detects changes to the mode or the hidden inactivity window", () => { | ||
| 158 | expect(hasChangedThreadSettlingSettings(DEFAULT_UNIFIED_SETTINGS)).toBe(false); | ||
| 159 | expect( | ||
| 160 | hasChangedThreadSettlingSettings({ | ||
| 161 | ...DEFAULT_UNIFIED_SETTINGS, | ||
| 162 | sidebarAutoSettleMode: "inactivity", | ||
| 163 | }), | ||
| 164 | ).toBe(true); | ||
| 165 | expect( | ||
| 166 | hasChangedThreadSettlingSettings({ | ||
| 167 | ...DEFAULT_UNIFIED_SETTINGS, | ||
| 168 | sidebarAutoSettleAfterDays: 30, | ||
| 169 | }), | ||
| 170 | ).toBe(true); | ||
| 171 | }); | ||
| 172 | }); | ||
| 173 | |||
| 174 | describe("formatDiagnosticsDescription", () => { | 155 | describe("formatDiagnosticsDescription", () => { |
| 175 | it("collapses trace and metric URLs that share the same OTEL base path", () => { | 156 | it("collapses trace and metric URLs that share the same OTEL base path", () => { |
| 176 | expect( | 157 | expect( |
apps/web/src/components/settings/SettingsPanels.logic.ts-9| ... | @@ -31,15 +31,6 @@ export function projectGroupingModeFromToggle( | ... | @@ -31,15 +31,6 @@ export function projectGroupingModeFromToggle( |
| 31 | return lastEnabledMode === "repository_path" ? "repository_path" : "repository"; | 31 | return lastEnabledMode === "repository_path" ? "repository_path" : "repository"; |
| 32 | } | 32 | } |
| 33 | 33 | ||
| 34 | export function hasChangedThreadSettlingSettings( | ||
| 35 | settings: Pick<UnifiedSettings, "sidebarAutoSettleMode" | "sidebarAutoSettleAfterDays">, | ||
| 36 | ): boolean { | ||
| 37 | return ( | ||
| 38 | settings.sidebarAutoSettleMode !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleMode || | ||
| 39 | settings.sidebarAutoSettleAfterDays !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays | ||
| 40 | ); | ||
| 41 | } | ||
| 42 | |||
| 43 | const LAST_ENABLED_PROJECT_GROUPING_MODE_KEY = "t3code:last-enabled-project-grouping-mode"; | 34 | const LAST_ENABLED_PROJECT_GROUPING_MODE_KEY = "t3code:last-enabled-project-grouping-mode"; |
| 44 | 35 | ||
| 45 | export function readLastEnabledProjectGroupingMode(): SidebarProjectGroupingMode { | 36 | export function readLastEnabledProjectGroupingMode(): SidebarProjectGroupingMode { |
apps/web/src/components/settings/SettingsPanels.tsx+53-46| ... | @@ -8,7 +8,6 @@ import { | ... | @@ -8,7 +8,6 @@ import { |
| 8 | type DesktopUpdateChannel, | 8 | type DesktopUpdateChannel, |
| 9 | ProviderDriverKind, | 9 | ProviderDriverKind, |
| 10 | type ScopedThreadRef, | 10 | type ScopedThreadRef, |
| 11 | type SidebarAutoSettleMode, | ||
| 12 | type SidebarProjectGroupingMode, | 11 | type SidebarProjectGroupingMode, |
| 13 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 14 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; | 13 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| ... | @@ -131,7 +130,6 @@ import { | ... | @@ -131,7 +130,6 @@ import { |
| 131 | normalizeIntervalSeconds, | 130 | normalizeIntervalSeconds, |
| 132 | PROVIDER_HEALTH_INTERVAL_STEP_SECONDS, | 131 | PROVIDER_HEALTH_INTERVAL_STEP_SECONDS, |
| 133 | hasChangedBackgroundActivitySettings, | 132 | hasChangedBackgroundActivitySettings, |
| 134 | hasChangedThreadSettlingSettings, | ||
| 135 | isProjectGroupingEnabled, | 133 | isProjectGroupingEnabled, |
| 136 | projectGroupingModeFromToggle, | 134 | projectGroupingModeFromToggle, |
| 137 | readLastEnabledProjectGroupingMode, | 135 | readLastEnabledProjectGroupingMode, |
| ... | @@ -473,7 +471,6 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -473,7 +471,6 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 473 | DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, | 471 | DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection ?? null, |
| 474 | ); | 472 | ); |
| 475 | const isBackgroundActivityDirty = hasChangedBackgroundActivitySettings(settings); | 473 | const isBackgroundActivityDirty = hasChangedBackgroundActivitySettings(settings); |
| 476 | const isThreadSettlingDirty = hasChangedThreadSettlingSettings(settings); | ||
| 477 | 474 | ||
| 478 | const changedSettingLabels = useMemo( | 475 | const changedSettingLabels = useMemo( |
| 479 | () => [ | 476 | () => [ |
| ... | @@ -498,7 +495,13 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -498,7 +495,13 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 498 | DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode | 495 | DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode |
| 499 | ? ["Project Grouping"] | 496 | ? ["Project Grouping"] |
| 500 | : []), | 497 | : []), |
| 501 | ...(isThreadSettlingDirty ? ["Thread settling"] : []), | 498 | ...(settings.sidebarAutoSettleAfterDays !== |
| 499 | DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays | ||
| 500 | ? ["Auto-settle inactive threads"] | ||
| 501 | : []), | ||
| 502 | ...(settings.sidebarAutoSettleOnMerge !== DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge | ||
| 503 | ? ["Auto-settle merged threads"] | ||
| 504 | : []), | ||
| 502 | ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), | 505 | ...(settings.wordWrap !== DEFAULT_UNIFIED_SETTINGS.wordWrap ? ["Word wrap"] : []), |
| 503 | ...getChangedTypographySettingLabels(settings), | 506 | ...getChangedTypographySettingLabels(settings), |
| 504 | ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace | 507 | ...(settings.diffIgnoreWhitespace !== DEFAULT_UNIFIED_SETTINGS.diffIgnoreWhitespace |
| ... | @@ -547,7 +550,6 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -547,7 +550,6 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 547 | [ | 550 | [ |
| 548 | isTextGenerationModelDirty, | 551 | isTextGenerationModelDirty, |
| 549 | isBackgroundActivityDirty, | 552 | isBackgroundActivityDirty, |
| 550 | isThreadSettlingDirty, | ||
| 551 | settings.browserDefaultViewport, | 553 | settings.browserDefaultViewport, |
| 552 | settings.browserDefaultZoomFactor, | 554 | settings.browserDefaultZoomFactor, |
| 553 | settings.browserDefaultAppearance, | 555 | settings.browserDefaultAppearance, |
| ... | @@ -574,6 +576,8 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -574,6 +576,8 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 574 | settings.glassOpacity, | 576 | settings.glassOpacity, |
| 575 | settings.enableLegacyTokenStreaming, | 577 | settings.enableLegacyTokenStreaming, |
| 576 | settings.enableProviderUpdateChecks, | 578 | settings.enableProviderUpdateChecks, |
| 579 | settings.sidebarAutoSettleAfterDays, | ||
| 580 | settings.sidebarAutoSettleOnMerge, | ||
| 577 | settings.sidebarProjectGroupingMode, | 581 | settings.sidebarProjectGroupingMode, |
| 578 | settings.sidebarThreadPreviewCount, | 582 | settings.sidebarThreadPreviewCount, |
| 579 | settings.showSkillsInSlashMenu, | 583 | settings.showSkillsInSlashMenu, |
| ... | @@ -658,7 +662,7 @@ export function useSettingsRestore(onRestored?: () => void) { | ... | @@ -658,7 +662,7 @@ export function useSettingsRestore(onRestored?: () => void) { |
| 658 | sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, | 662 | sidebarThreadPreviewCount: DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount, |
| 659 | sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, | 663 | sidebarProjectGroupingMode: DEFAULT_UNIFIED_SETTINGS.sidebarProjectGroupingMode, |
| 660 | sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, | 664 | sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, |
| 661 | sidebarAutoSettleMode: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleMode, | 665 | sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, |
| 662 | enableLegacyTokenStreaming: DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming, | 666 | enableLegacyTokenStreaming: DEFAULT_UNIFIED_SETTINGS.enableLegacyTokenStreaming, |
| 663 | enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, | 667 | enableProviderUpdateChecks: DEFAULT_UNIFIED_SETTINGS.enableProviderUpdateChecks, |
| 664 | backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, | 668 | backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity, |
| ... | @@ -1686,11 +1690,6 @@ function FontFamilySettingsRow({ | ... | @@ -1686,11 +1690,6 @@ function FontFamilySettingsRow({ |
| 1686 | } | 1690 | } |
| 1687 | 1691 | ||
| 1688 | const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ?? 3; | 1692 | const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ?? 3; |
| 1689 | const AUTO_SETTLE_MODE_LABELS: Readonly<Record<SidebarAutoSettleMode, string>> = { | ||
| 1690 | never: "Never (manual only)", | ||
| 1691 | "change-request": "When PR merges or closes", | ||
| 1692 | inactivity: "After inactivity", | ||
| 1693 | }; | ||
| 1694 | 1693 | ||
| 1695 | function AutoSettleDaysInput({ | 1694 | function AutoSettleDaysInput({ |
| 1696 | value, | 1695 | value, |
| ... | @@ -1952,59 +1951,67 @@ export function GeneralSettingsPanel() { | ... | @@ -1952,59 +1951,67 @@ export function GeneralSettingsPanel() { |
| 1952 | /> | 1951 | /> |
| 1953 | 1952 | ||
| 1954 | <SettingsRow | 1953 | <SettingsRow |
| 1955 | {...searchableSetting("thread-settling")} | 1954 | {...searchableSetting("auto-settle-merged-threads")} |
| 1956 | description="Choose the only event allowed to settle a thread automatically. Manual settling always remains available." | 1955 | description="Settle a thread when its pull request merges. Closed pull requests still settle automatically." |
| 1957 | resetAction={ | 1956 | resetAction={ |
| 1958 | hasChangedThreadSettlingSettings(settings) ? ( | 1957 | settings.sidebarAutoSettleOnMerge !== |
| 1958 | DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge ? ( | ||
| 1959 | <SettingResetButton | 1959 | <SettingResetButton |
| 1960 | label="thread settling" | 1960 | label="auto-settle on merge" |
| 1961 | onClick={() => | 1961 | onClick={() => |
| 1962 | updateSettings({ | 1962 | updateSettings({ |
| 1963 | sidebarAutoSettleMode: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleMode, | 1963 | sidebarAutoSettleOnMerge: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleOnMerge, |
| 1964 | sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, | ||
| 1965 | }) | 1964 | }) |
| 1966 | } | 1965 | } |
| 1967 | /> | 1966 | /> |
| 1968 | ) : null | 1967 | ) : null |
| 1969 | } | 1968 | } |
| 1970 | control={ | 1969 | control={ |
| 1971 | <Select | 1970 | <Switch |
| 1972 | value={settings.sidebarAutoSettleMode} | 1971 | checked={settings.sidebarAutoSettleOnMerge} |
| 1973 | onValueChange={(value) => { | 1972 | onCheckedChange={(checked) => |
| 1974 | if (value === "never" || value === "change-request" || value === "inactivity") { | 1973 | updateSettings({ sidebarAutoSettleOnMerge: Boolean(checked) }) |
| 1974 | } | ||
| 1975 | aria-label="Auto-settle merged threads" | ||
| 1976 | /> | ||
| 1977 | } | ||
| 1978 | /> | ||
| 1979 | |||
| 1980 | <SettingsRow | ||
| 1981 | {...searchableSetting("auto-settle-inactive-threads")} | ||
| 1982 | description="Sidebar threads with no activity for this long settle automatically." | ||
| 1983 | resetAction={ | ||
| 1984 | settings.sidebarAutoSettleAfterDays !== | ||
| 1985 | DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ? ( | ||
| 1986 | <SettingResetButton | ||
| 1987 | label="auto-settle" | ||
| 1988 | onClick={() => | ||
| 1975 | updateSettings({ | 1989 | updateSettings({ |
| 1976 | sidebarAutoSettleMode: value, | 1990 | sidebarAutoSettleAfterDays: DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays, |
| 1977 | ...(value === "inactivity" && settings.sidebarAutoSettleAfterDays === null | 1991 | }) |
| 1978 | ? { sidebarAutoSettleAfterDays: AUTO_SETTLE_DEFAULT_DAYS } | ||
| 1979 | : {}), | ||
| 1980 | }); | ||
| 1981 | } | 1992 | } |
| 1982 | }} | 1993 | /> |
| 1983 | > | 1994 | ) : null |
| 1984 | <SelectTrigger className="w-full sm:w-64" aria-label="Thread settling"> | 1995 | } |
| 1985 | <SelectValue>{AUTO_SETTLE_MODE_LABELS[settings.sidebarAutoSettleMode]}</SelectValue> | 1996 | control={ |
| 1986 | </SelectTrigger> | 1997 | <Switch |
| 1987 | <SelectPopup align="end" alignItemWithTrigger={false}> | 1998 | checked={settings.sidebarAutoSettleAfterDays !== null} |
| 1988 | <SelectItem hideIndicator value="never"> | 1999 | onCheckedChange={(checked) => |
| 1989 | {AUTO_SETTLE_MODE_LABELS.never} | 2000 | updateSettings({ |
| 1990 | </SelectItem> | 2001 | sidebarAutoSettleAfterDays: checked ? AUTO_SETTLE_DEFAULT_DAYS : null, |
| 1991 | <SelectItem hideIndicator value="change-request"> | 2002 | }) |
| 1992 | {AUTO_SETTLE_MODE_LABELS["change-request"]} | 2003 | } |
| 1993 | </SelectItem> | 2004 | aria-label="Auto-settle inactive threads" |
| 1994 | <SelectItem hideIndicator value="inactivity"> | 2005 | /> |
| 1995 | {AUTO_SETTLE_MODE_LABELS.inactivity} | ||
| 1996 | </SelectItem> | ||
| 1997 | </SelectPopup> | ||
| 1998 | </Select> | ||
| 1999 | } | 2006 | } |
| 2000 | /> | 2007 | /> |
| 2001 | {settings.sidebarAutoSettleMode === "inactivity" ? ( | 2008 | {settings.sidebarAutoSettleAfterDays !== null ? ( |
| 2002 | <SettingsRow | 2009 | <SettingsRow |
| 2003 | title="Days of inactivity before auto-settle" | 2010 | title="Days of inactivity before auto-settle" |
| 2004 | description="Running, blocked, and open-pull-request threads always stay active." | 2011 | description="Any new activity un-settles a thread automatically." |
| 2005 | control={ | 2012 | control={ |
| 2006 | <AutoSettleDaysInput | 2013 | <AutoSettleDaysInput |
| 2007 | value={settings.sidebarAutoSettleAfterDays ?? AUTO_SETTLE_DEFAULT_DAYS} | 2014 | value={settings.sidebarAutoSettleAfterDays} |
| 2008 | onCommit={(days) => updateSettings({ sidebarAutoSettleAfterDays: days })} | 2015 | onCommit={(days) => updateSettings({ sidebarAutoSettleAfterDays: days })} |
| 2009 | /> | 2016 | /> |
| 2010 | } | 2017 | } |
apps/web/src/components/settings/settingsSearch.ts+7-2| ... | @@ -112,8 +112,13 @@ export const SETTINGS_SEARCH_ITEMS = [ | ... | @@ -112,8 +112,13 @@ export const SETTINGS_SEARCH_ITEMS = [ |
| 112 | to: "/settings/general", | 112 | to: "/settings/general", |
| 113 | }, | 113 | }, |
| 114 | { | 114 | { |
| 115 | id: "thread-settling", | 115 | id: "auto-settle-inactive-threads", |
| 116 | title: "Thread settling", | 116 | title: "Auto-settle inactive threads", |
| 117 | to: "/settings/general", | ||
| 118 | }, | ||
| 119 | { | ||
| 120 | id: "auto-settle-merged-threads", | ||
| 121 | title: "Auto-settle merged threads", | ||
| 117 | to: "/settings/general", | 122 | to: "/settings/general", |
| 118 | }, | 123 | }, |
| 119 | { | 124 | { |
apps/web/src/hooks/useThreadActionMenu.ts+3-3| ... | @@ -81,7 +81,7 @@ export function useThreadActionMenu(input: { | ... | @@ -81,7 +81,7 @@ export function useThreadActionMenu(input: { |
| 81 | const handleNewThread = useNewThreadHandler(); | 81 | const handleNewThread = useNewThreadHandler(); |
| 82 | const markThreadUnread = useUiStateStore((s) => s.markThreadUnread); | 82 | const markThreadUnread = useUiStateStore((s) => s.markThreadUnread); |
| 83 | const autoSettleAfterDays = useClientSettings((s) => s.sidebarAutoSettleAfterDays); | 83 | const autoSettleAfterDays = useClientSettings((s) => s.sidebarAutoSettleAfterDays); |
| 84 | const autoSettleMode = useClientSettings((s) => s.sidebarAutoSettleMode); | 84 | const autoSettleOnMerge = useClientSettings((s) => s.sidebarAutoSettleOnMerge); |
| 85 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); | 85 | const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); |
| 86 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); | 86 | const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); |
| 87 | const timestampFormat = useClientSettings((s) => s.timestampFormat); | 87 | const timestampFormat = useClientSettings((s) => s.timestampFormat); |
| ... | @@ -135,7 +135,7 @@ export function useThreadActionMenu(input: { | ... | @@ -135,7 +135,7 @@ export function useThreadActionMenu(input: { |
| 135 | // parked-thread banner within the same minute. | 135 | // parked-thread banner within the same minute. |
| 136 | now: `${now.toISOString().slice(0, 16)}:00.000Z`, | 136 | now: `${now.toISOString().slice(0, 16)}:00.000Z`, |
| 137 | autoSettleAfterDays, | 137 | autoSettleAfterDays, |
| 138 | autoSettleMode, | 138 | autoSettleOnMerge, |
| 139 | changeRequest, | 139 | changeRequest, |
| 140 | }), | 140 | }), |
| 141 | isSnoozed: supports.snooze && effectiveSnoozed(thread, { now: now.toISOString() }), | 141 | isSnoozed: supports.snooze && effectiveSnoozed(thread, { now: now.toISOString() }), |
| ... | @@ -312,7 +312,7 @@ export function useThreadActionMenu(input: { | ... | @@ -312,7 +312,7 @@ export function useThreadActionMenu(input: { |
| 312 | [ | 312 | [ |
| 313 | archiveThread, | 313 | archiveThread, |
| 314 | autoSettleAfterDays, | 314 | autoSettleAfterDays, |
| 315 | autoSettleMode, | 315 | autoSettleOnMerge, |
| 316 | changeRequest, | 316 | changeRequest, |
| 317 | confirmThreadArchive, | 317 | confirmThreadArchive, |
| 318 | confirmThreadDelete, | 318 | confirmThreadDelete, |
docs/user/thread-sidebar.md+4-8| ... | @@ -7,19 +7,15 @@ their project, including when you connect to more than one environment. | ... | @@ -7,19 +7,15 @@ their project, including when you connect to more than one environment. |
| 7 | To require confirmation before unpinning, enable **Settings → General → Unpin confirmation**. The | 7 | To require confirmation before unpinning, enable **Settings → General → Unpin confirmation**. The |
| 8 | confirmation applies to the sidebar controls, thread menus, and the `mod+shift+p` shortcut. | 8 | confirmation applies to the sidebar controls, thread menus, and the `mod+shift+p` shortcut. |
| 9 | 9 | ||
| 10 | Pinned threads follow the auto-settle policy in **Settings → General**. Choose exactly one policy: | 10 | Pinned threads still move to **Settled** when they become inactive. They also move when their pull |
| 11 | settle when a pull request merges or closes, settle after a configurable period of inactivity, or | 11 | request merges if **Auto-settle merged threads** is enabled. |
| 12 | never auto-settle. **Never** leaves every automatic path off, so threads settle only when you choose | ||
| 13 | **Settle**. | ||
| 14 | 12 | ||
| 15 | When you un-settle a thread, it returns to the top of the active list so you can find it right | 13 | When you un-settle a thread, it returns to the top of the active list so you can find it right |
| 16 | away. Its timestamps do not change. Other threads keep their positions. | 14 | away. Its timestamps do not change. Other threads keep their positions. |
| 17 | 15 | ||
| 18 | Right-click a pull request link in a thread and choose **Link to thread** to show that pull request | 16 | Right-click a pull request link in a thread and choose **Link to thread** to show that pull request |
| 19 | in the sidebar. The thread settles when the linked pull request merges or closes only when the | 17 | in the sidebar. The thread settles when the linked pull request merges if **Auto-settle merged |
| 20 | pull-request policy is selected. Right-click the same link and choose **Unlink from thread** to | 18 | threads** is enabled. Right-click the same link and choose **Unlink from thread** to remove it. |
| 21 | remove it. If T3 Code cannot determine when a pull request entered its terminal state, it keeps the | ||
| 22 | thread active. | ||
| 23 | 19 | ||
| 24 | On web and desktop, drag a pinned thread to change its position. On mobile, open the thread's menu | 20 | On web and desktop, drag a pinned thread to change its position. On mobile, open the thread's menu |
| 25 | and choose **Move up** or **Move down**. The order is stored by the server and appears on your | 21 | and choose **Move up** or **Move down**. The order is stored by the server and appears on your |
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.ts-1| ... | @@ -17,7 +17,6 @@ const THEME_INTEROP_ALLOWLIST = new Set([ | ... | @@ -17,7 +17,6 @@ const THEME_INTEROP_ALLOWLIST = new Set([ |
| 17 | "features/home/HomeHeader.tsx", | 17 | "features/home/HomeHeader.tsx", |
| 18 | "features/review/ReviewSheet.tsx", | 18 | "features/review/ReviewSheet.tsx", |
| 19 | "features/review/useNativeReviewDiffBridge.ts", | 19 | "features/review/useNativeReviewDiffBridge.ts", |
| 20 | "features/settings/SettingsAutoSettleRouteScreen.tsx", | ||
| 21 | "features/settings/SettingsEnvironmentsRouteScreen.tsx", | 20 | "features/settings/SettingsEnvironmentsRouteScreen.tsx", |
| 22 | "features/settings/appearance/components/AppearancePreviews.tsx", | 21 | "features/settings/appearance/components/AppearancePreviews.tsx", |
| 23 | "features/settings/appearance/components/FontSizeSliderRow.tsx", | 22 | "features/settings/appearance/components/FontSizeSliderRow.tsx", |
packages/client-runtime/src/state/threadSettled.test.ts+74-116| ... | @@ -21,6 +21,18 @@ const FRESH = "2026-04-09T00:00:00.000Z"; | ... | @@ -21,6 +21,18 @@ const FRESH = "2026-04-09T00:00:00.000Z"; |
| 21 | const STALE = "2026-04-06T23:59:59.999Z"; | 21 | const STALE = "2026-04-06T23:59:59.999Z"; |
| 22 | 22 | ||
| 23 | describe("changeRequestAutoSettles", () => { | 23 | describe("changeRequestAutoSettles", () => { |
| 24 | it.each([ | ||
| 25 | ["open", true, false], | ||
| 26 | ["merged", true, true], | ||
| 27 | ["merged", false, false], | ||
| 28 | ["closed", false, true], | ||
| 29 | [null, false, false], | ||
| 30 | ] as const)("state=%s autoSettleOnMerge=%s returns %s", (state, autoSettleOnMerge, expected) => { | ||
| 31 | expect(changeRequestAutoSettles(state === null ? null : { state }, { autoSettleOnMerge })).toBe( | ||
| 32 | expected, | ||
| 33 | ); | ||
| 34 | }); | ||
| 35 | |||
| 24 | const THREAD_CREATED_AT = "2026-04-01T00:00:00.000Z"; | 36 | const THREAD_CREATED_AT = "2026-04-01T00:00:00.000Z"; |
| 25 | const idleThread = { | 37 | const idleThread = { |
| 26 | createdAt: THREAD_CREATED_AT, | 38 | createdAt: THREAD_CREATED_AT, |
| ... | @@ -28,19 +40,6 @@ describe("changeRequestAutoSettles", () => { | ... | @@ -28,19 +40,6 @@ describe("changeRequestAutoSettles", () => { |
| 28 | latestTurn: null, | 40 | latestTurn: null, |
| 29 | }; | 41 | }; |
| 30 | 42 | ||
| 31 | it.each([ | ||
| 32 | ["open", false], | ||
| 33 | ["merged", true], | ||
| 34 | ["closed", true], | ||
| 35 | [null, false], | ||
| 36 | ] as const)("state=%s returns %s", (state, expected) => { | ||
| 37 | expect( | ||
| 38 | changeRequestAutoSettles(state === null ? null : { state, updatedAt: THREAD_CREATED_AT }, { | ||
| 39 | thread: idleThread, | ||
| 40 | }), | ||
| 41 | ).toBe(expected); | ||
| 42 | }); | ||
| 43 | |||
| 44 | it("ignores a terminal change request last touched before the thread existed", () => { | 43 | it("ignores a terminal change request last touched before the thread existed", () => { |
| 45 | for (const state of ["merged", "closed"] as const) { | 44 | for (const state of ["merged", "closed"] as const) { |
| 46 | expect( | 45 | expect( |
| ... | @@ -106,20 +105,20 @@ describe("changeRequestAutoSettles", () => { | ... | @@ -106,20 +105,20 @@ describe("changeRequestAutoSettles", () => { |
| 106 | ).toBe(true); | 105 | ).toBe(true); |
| 107 | }); | 106 | }); |
| 108 | 107 | ||
| 109 | it("keeps the thread active when either timestamp is missing or malformed", () => { | 108 | it("falls back to settling when either timestamp is missing or malformed", () => { |
| 110 | expect(changeRequestAutoSettles({ state: "merged" }, { thread: idleThread })).toBe(false); | 109 | expect(changeRequestAutoSettles({ state: "merged" }, { thread: idleThread })).toBe(true); |
| 111 | expect( | 110 | expect( |
| 112 | changeRequestAutoSettles({ state: "merged", updatedAt: null }, { thread: idleThread }), | 111 | changeRequestAutoSettles({ state: "merged", updatedAt: null }, { thread: idleThread }), |
| 113 | ).toBe(false); | 112 | ).toBe(true); |
| 114 | expect( | 113 | expect( |
| 115 | changeRequestAutoSettles({ state: "merged", updatedAt: "2026-03-01T00:00:00.000Z" }, {}), | 114 | changeRequestAutoSettles({ state: "merged", updatedAt: "2026-03-01T00:00:00.000Z" }, {}), |
| 116 | ).toBe(false); | 115 | ).toBe(true); |
| 117 | expect( | 116 | expect( |
| 118 | changeRequestAutoSettles( | 117 | changeRequestAutoSettles( |
| 119 | { state: "merged", updatedAt: "not-a-date" }, | 118 | { state: "merged", updatedAt: "not-a-date" }, |
| 120 | { thread: idleThread }, | 119 | { thread: idleThread }, |
| 121 | ), | 120 | ), |
| 122 | ).toBe(false); | 121 | ).toBe(true); |
| 123 | }); | 122 | }); |
| 124 | }); | 123 | }); |
| 125 | 124 | ||
| ... | @@ -205,47 +204,38 @@ describe("effectiveSettled", () => { | ... | @@ -205,47 +204,38 @@ describe("effectiveSettled", () => { |
| 205 | ] as const; | 204 | ] as const; |
| 206 | const runningCases = [false, true] as const; | 205 | const runningCases = [false, true] as const; |
| 207 | const pendingCases = [undefined, "approval", "user-input"] as const; | 206 | const pendingCases = [undefined, "approval", "user-input"] as const; |
| 208 | const autoSettleModes = ["never", "change-request", "inactivity"] as const; | 207 | const truthTable = overrideCases.flatMap((settledOverride) => |
| 209 | const truthTable = autoSettleModes.flatMap((autoSettleMode) => | 208 | changeRequestStates.flatMap((changeRequestState) => |
| 210 | overrideCases.flatMap((settledOverride) => | 209 | inactivityCases.flatMap(([inactivity, activityAt]) => |
| 211 | changeRequestStates.flatMap((changeRequestState) => | 210 | runningCases.flatMap((running) => |
| 212 | inactivityCases.flatMap(([inactivity, activityAt]) => | 211 | pendingCases.map((pending) => ({ |
| 213 | runningCases.flatMap((running) => | 212 | settledOverride, |
| 214 | pendingCases.map((pending) => ({ | 213 | changeRequestState, |
| 215 | autoSettleMode, | 214 | inactivity, |
| 216 | settledOverride, | 215 | activityAt, |
| 217 | changeRequestState, | 216 | running, |
| 218 | inactivity, | 217 | pending, |
| 219 | activityAt, | 218 | // Settled iff nothing blocks (pending work / live session) AND |
| 220 | running, | 219 | // the override says settled, or (with no override) a merged PR |
| 221 | pending, | 220 | // or staleness auto-settles. The "active" pin suppresses both |
| 222 | expected: | 221 | // auto signals, and an open PR suppresses the inactivity path: |
| 223 | pending === undefined && | 222 | // a thread with a PR out for review is never done, however quiet. |
| 224 | !running && | 223 | expected: |
| 225 | (settledOverride === "settled" || | 224 | pending === undefined && |
| 226 | (settledOverride === null && | 225 | !running && |
| 227 | ((autoSettleMode === "change-request" && changeRequestState === "merged") || | 226 | (settledOverride === "settled" || |
| 228 | (autoSettleMode === "inactivity" && | 227 | (settledOverride === null && |
| 229 | changeRequestState !== "open" && | 228 | (changeRequestState === "merged" || |
| 230 | inactivity === "stale")))), | 229 | (changeRequestState !== "open" && inactivity === "stale")))), |
| 231 | })), | 230 | })), |
| 232 | ), | ||
| 233 | ), | 231 | ), |
| 234 | ), | 232 | ), |
| 235 | ), | 233 | ), |
| 236 | ); | 234 | ); |
| 237 | 235 | ||
| 238 | it.each(truthTable)( | 236 | it.each(truthTable)( |
| 239 | "mode=$autoSettleMode override=$settledOverride pr=$changeRequestState inactivity=$inactivity running=$running pending=$pending", | 237 | "override=$settledOverride pr=$changeRequestState inactivity=$inactivity running=$running pending=$pending", |
| 240 | ({ | 238 | ({ settledOverride, changeRequestState, activityAt, running, pending, expected }) => { |
| 241 | autoSettleMode, | ||
| 242 | settledOverride, | ||
| 243 | changeRequestState, | ||
| 244 | activityAt, | ||
| 245 | running, | ||
| 246 | pending, | ||
| 247 | expected, | ||
| 248 | }) => { | ||
| 249 | const shell = makeShell({ | 239 | const shell = makeShell({ |
| 250 | settledOverride, | 240 | settledOverride, |
| 251 | activityAt, | 241 | activityAt, |
| ... | @@ -255,18 +245,12 @@ describe("effectiveSettled", () => { | ... | @@ -255,18 +245,12 @@ describe("effectiveSettled", () => { |
| 255 | const changeRequestOptions = | 245 | const changeRequestOptions = |
| 256 | changeRequestState === undefined | 246 | changeRequestState === undefined |
| 257 | ? {} | 247 | ? {} |
| 258 | : { | 248 | : { changeRequest: { state: changeRequestState as ChangeRequestStateLike } }; |
| 259 | changeRequest: { | ||
| 260 | state: changeRequestState as ChangeRequestStateLike, | ||
| 261 | updatedAt: NOW, | ||
| 262 | }, | ||
| 263 | }; | ||
| 264 | 249 | ||
| 265 | expect( | 250 | expect( |
| 266 | effectiveSettled(shell, { | 251 | effectiveSettled(shell, { |
| 267 | now: NOW, | 252 | now: NOW, |
| 268 | autoSettleAfterDays: 3, | 253 | autoSettleAfterDays: 3, |
| 269 | autoSettleMode, | ||
| 270 | ...changeRequestOptions, | 254 | ...changeRequestOptions, |
| 271 | }), | 255 | }), |
| 272 | ).toBe(expected); | 256 | ).toBe(expected); |
| ... | @@ -279,8 +263,7 @@ describe("effectiveSettled", () => { | ... | @@ -279,8 +263,7 @@ describe("effectiveSettled", () => { |
| 279 | effectiveSettled(shell, { | 263 | effectiveSettled(shell, { |
| 280 | now: NOW, | 264 | now: NOW, |
| 281 | autoSettleAfterDays: null, | 265 | autoSettleAfterDays: null, |
| 282 | autoSettleMode: "change-request", | 266 | changeRequest: { state: "closed" }, |
| 283 | changeRequest: { state: "closed", updatedAt: NOW }, | ||
| 284 | }), | 267 | }), |
| 285 | ).toBe(true); | 268 | ).toBe(true); |
| 286 | }); | 269 | }); |
| ... | @@ -292,8 +275,7 @@ describe("effectiveSettled", () => { | ... | @@ -292,8 +275,7 @@ describe("effectiveSettled", () => { |
| 292 | effectiveSettled(recentlyActive, { | 275 | effectiveSettled(recentlyActive, { |
| 293 | now: NOW, | 276 | now: NOW, |
| 294 | autoSettleAfterDays: null, | 277 | autoSettleAfterDays: null, |
| 295 | autoSettleMode: "change-request", | 278 | changeRequest: { state: changeRequestState }, |
| 296 | changeRequest: { state: changeRequestState, updatedAt: NOW }, | ||
| 297 | }), | 279 | }), |
| 298 | ).toBe(true); | 280 | ).toBe(true); |
| 299 | } | 281 | } |
| ... | @@ -309,7 +291,6 @@ describe("effectiveSettled", () => { | ... | @@ -309,7 +291,6 @@ describe("effectiveSettled", () => { |
| 309 | effectiveSettled(fresh, { | 291 | effectiveSettled(fresh, { |
| 310 | now: NOW, | 292 | now: NOW, |
| 311 | autoSettleAfterDays: null, | 293 | autoSettleAfterDays: null, |
| 312 | autoSettleMode: "change-request", | ||
| 313 | changeRequest: { state, updatedAt: "2026-03-20T00:00:00.000Z" }, | 294 | changeRequest: { state, updatedAt: "2026-03-20T00:00:00.000Z" }, |
| 314 | }), | 295 | }), |
| 315 | ).toBe(false); | 296 | ).toBe(false); |
| ... | @@ -319,24 +300,30 @@ describe("effectiveSettled", () => { | ... | @@ -319,24 +300,30 @@ describe("effectiveSettled", () => { |
| 319 | effectiveSettled(fresh, { | 300 | effectiveSettled(fresh, { |
| 320 | now: NOW, | 301 | now: NOW, |
| 321 | autoSettleAfterDays: null, | 302 | autoSettleAfterDays: null, |
| 322 | autoSettleMode: "change-request", | ||
| 323 | changeRequest: { state: "merged", updatedAt: "2026-04-09T00:00:00.000Z" }, | 303 | changeRequest: { state: "merged", updatedAt: "2026-04-09T00:00:00.000Z" }, |
| 324 | }), | 304 | }), |
| 325 | ).toBe(true); | 305 | ).toBe(true); |
| 326 | }); | 306 | }); |
| 327 | 307 | ||
| 328 | it("keeps a merged change request active in every other mode", () => { | 308 | it("can keep a merged change request active", () => { |
| 329 | const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" }); | 309 | const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" }); |
| 330 | for (const autoSettleMode of ["never", "inactivity"] as const) { | 310 | expect( |
| 331 | expect( | 311 | effectiveSettled(recentlyActive, { |
| 332 | effectiveSettled(recentlyActive, { | 312 | now: NOW, |
| 333 | now: NOW, | 313 | autoSettleAfterDays: null, |
| 334 | autoSettleAfterDays: null, | 314 | autoSettleOnMerge: false, |
| 335 | autoSettleMode, | 315 | changeRequest: { state: "merged" }, |
| 336 | changeRequest: { state: "merged", updatedAt: NOW }, | 316 | }), |
| 337 | }), | 317 | ).toBe(false); |
| 338 | ).toBe(false); | 318 | |
| 339 | } | 319 | expect( |
| 320 | effectiveSettled(recentlyActive, { | ||
| 321 | now: NOW, | ||
| 322 | autoSettleAfterDays: null, | ||
| 323 | autoSettleOnMerge: false, | ||
| 324 | changeRequest: { state: "closed" }, | ||
| 325 | }), | ||
| 326 | ).toBe(true); | ||
| 340 | }); | 327 | }); |
| 341 | 328 | ||
| 342 | it("never auto-settles a stale thread with an open change request", () => { | 329 | it("never auto-settles a stale thread with an open change request", () => { |
| ... | @@ -345,7 +332,6 @@ describe("effectiveSettled", () => { | ... | @@ -345,7 +332,6 @@ describe("effectiveSettled", () => { |
| 345 | effectiveSettled(stale, { | 332 | effectiveSettled(stale, { |
| 346 | now: NOW, | 333 | now: NOW, |
| 347 | autoSettleAfterDays: 3, | 334 | autoSettleAfterDays: 3, |
| 348 | autoSettleMode: "inactivity", | ||
| 349 | changeRequest: { state: "open" }, | 335 | changeRequest: { state: "open" }, |
| 350 | }), | 336 | }), |
| 351 | ).toBe(false); | 337 | ).toBe(false); |
| ... | @@ -355,7 +341,6 @@ describe("effectiveSettled", () => { | ... | @@ -355,7 +341,6 @@ describe("effectiveSettled", () => { |
| 355 | effectiveSettled(settled, { | 341 | effectiveSettled(settled, { |
| 356 | now: NOW, | 342 | now: NOW, |
| 357 | autoSettleAfterDays: 3, | 343 | autoSettleAfterDays: 3, |
| 358 | autoSettleMode: "inactivity", | ||
| 359 | changeRequest: { state: "open" }, | 344 | changeRequest: { state: "open" }, |
| 360 | }), | 345 | }), |
| 361 | ).toBe(true); | 346 | ).toBe(true); |
| ... | @@ -370,8 +355,7 @@ describe("effectiveSettled", () => { | ... | @@ -370,8 +355,7 @@ describe("effectiveSettled", () => { |
| 370 | effectiveSettled(shell, { | 355 | effectiveSettled(shell, { |
| 371 | now: NOW, | 356 | now: NOW, |
| 372 | autoSettleAfterDays: null, | 357 | autoSettleAfterDays: null, |
| 373 | autoSettleMode: "change-request", | 358 | changeRequest: { state: "merged" }, |
| 374 | changeRequest: { state: "merged", updatedAt: NOW }, | ||
| 375 | }), | 359 | }), |
| 376 | ).toBe(false); | 360 | ).toBe(false); |
| 377 | }); | 361 | }); |
| ... | @@ -386,8 +370,7 @@ describe("effectiveSettled", () => { | ... | @@ -386,8 +370,7 @@ describe("effectiveSettled", () => { |
| 386 | effectiveSettled(shell, { | 370 | effectiveSettled(shell, { |
| 387 | now: NOW, | 371 | now: NOW, |
| 388 | autoSettleAfterDays: 3, | 372 | autoSettleAfterDays: 3, |
| 389 | autoSettleMode: "change-request", | 373 | changeRequest: { state: "merged" }, |
| 390 | changeRequest: { state: "merged", updatedAt: NOW }, | ||
| 391 | }), | 374 | }), |
| 392 | ).toBe(false); | 375 | ).toBe(false); |
| 393 | }); | 376 | }); |
| ... | @@ -431,8 +414,7 @@ describe("effectiveSettled", () => { | ... | @@ -431,8 +414,7 @@ describe("effectiveSettled", () => { |
| 431 | effectiveSettled(shell, { | 414 | effectiveSettled(shell, { |
| 432 | now: transitionNow, | 415 | now: transitionNow, |
| 433 | autoSettleAfterDays: 3, | 416 | autoSettleAfterDays: 3, |
| 434 | autoSettleMode: "change-request", | 417 | changeRequest: { state: "merged" }, |
| 435 | changeRequest: { state: "merged", updatedAt: transitionNow }, | ||
| 436 | }), | 418 | }), |
| 437 | ).toBe(false); | 419 | ).toBe(false); |
| 438 | } | 420 | } |
| ... | @@ -444,20 +426,8 @@ describe("effectiveSettled", () => { | ... | @@ -444,20 +426,8 @@ describe("effectiveSettled", () => { |
| 444 | }); | 426 | }); |
| 445 | const stale = makeShell({ activityAt: STALE }); | 427 | const stale = makeShell({ activityAt: STALE }); |
| 446 | 428 | ||
| 447 | expect( | 429 | expect(effectiveSettled(boundary, { now: NOW, autoSettleAfterDays: 3 })).toBe(false); |
| 448 | effectiveSettled(boundary, { | 430 | expect(effectiveSettled(stale, { now: NOW, autoSettleAfterDays: null })).toBe(false); |
| 449 | now: NOW, | ||
| 450 | autoSettleAfterDays: 3, | ||
| 451 | autoSettleMode: "inactivity", | ||
| 452 | }), | ||
| 453 | ).toBe(false); | ||
| 454 | expect( | ||
| 455 | effectiveSettled(stale, { | ||
| 456 | now: NOW, | ||
| 457 | autoSettleAfterDays: null, | ||
| 458 | autoSettleMode: "inactivity", | ||
| 459 | }), | ||
| 460 | ).toBe(false); | ||
| 461 | }); | 431 | }); |
| 462 | }); | 432 | }); |
| 463 | 433 | ||
| ... | @@ -562,8 +532,7 @@ describe("canSettle", () => { | ... | @@ -562,8 +532,7 @@ describe("canSettle", () => { |
| 562 | effectiveSettled(queued, { | 532 | effectiveSettled(queued, { |
| 563 | now: justAfter, | 533 | now: justAfter, |
| 564 | autoSettleAfterDays: 3, | 534 | autoSettleAfterDays: 3, |
| 565 | autoSettleMode: "change-request", | 535 | changeRequest: { state: "merged" }, |
| 566 | changeRequest: { state: "merged", updatedAt: justAfter }, | ||
| 567 | }), | 536 | }), |
| 568 | ).toBe(false); | 537 | ).toBe(false); |
| 569 | // Past the window the message is a failed/stale start: settleable again. | 538 | // Past the window the message is a failed/stale start: settleable again. |
| ... | @@ -585,13 +554,9 @@ describe("canSettle", () => { | ... | @@ -585,13 +554,9 @@ describe("canSettle", () => { |
| 585 | settledAt: "2026-04-09T12:02:10.000Z", | 554 | settledAt: "2026-04-09T12:02:10.000Z", |
| 586 | }; | 555 | }; |
| 587 | expect(hasQueuedTurnStart(settledAfterMessage, { now: flooredNow })).toBe(true); | 556 | expect(hasQueuedTurnStart(settledAfterMessage, { now: flooredNow })).toBe(true); |
| 588 | expect( | 557 | expect(effectiveSettled(settledAfterMessage, { now: flooredNow, autoSettleAfterDays: 3 })).toBe( |
| 589 | effectiveSettled(settledAfterMessage, { | 558 | true, |
| 590 | now: flooredNow, | 559 | ); |
| 591 | autoSettleAfterDays: 3, | ||
| 592 | autoSettleMode: "never", | ||
| 593 | }), | ||
| 594 | ).toBe(true); | ||
| 595 | 560 | ||
| 596 | // A message NEWER than settledAt is genuinely new work: still blocked | 561 | // A message NEWER than settledAt is genuinely new work: still blocked |
| 597 | // until the server's auto-unsettle lands. | 562 | // until the server's auto-unsettle lands. |
| ... | @@ -604,7 +569,6 @@ describe("canSettle", () => { | ... | @@ -604,7 +569,6 @@ describe("canSettle", () => { |
| 604 | effectiveSettled(messageAfterSettle, { | 569 | effectiveSettled(messageAfterSettle, { |
| 605 | now: "2026-04-09T12:03:30.000Z", | 570 | now: "2026-04-09T12:03:30.000Z", |
| 606 | autoSettleAfterDays: 3, | 571 | autoSettleAfterDays: 3, |
| 607 | autoSettleMode: "never", | ||
| 608 | }), | 572 | }), |
| 609 | ).toBe(false); | 573 | ).toBe(false); |
| 610 | }); | 574 | }); |
| ... | @@ -618,12 +582,6 @@ describe("canSettle", () => { | ... | @@ -618,12 +582,6 @@ describe("canSettle", () => { |
| 618 | pending: "user-input", | 582 | pending: "user-input", |
| 619 | }); | 583 | }); |
| 620 | expect(canSettle(blocked, { now: NOW })).toBe(false); | 584 | expect(canSettle(blocked, { now: NOW })).toBe(false); |
| 621 | expect( | 585 | expect(effectiveSettled(blocked, { now: NOW, autoSettleAfterDays: 3 })).toBe(false); |
| 622 | effectiveSettled(blocked, { | ||
| 623 | now: NOW, | ||
| 624 | autoSettleAfterDays: 3, | ||
| 625 | autoSettleMode: "never", | ||
| 626 | }), | ||
| 627 | ).toBe(false); | ||
| 628 | }); | 586 | }); |
| 629 | }); | 587 | }); |
packages/client-runtime/src/state/threadSettled.ts+25-17| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | // @effect-diagnostics globalDate:off -- UI snooze presets use local calendar boundaries and Intl labels. | 1 | // @effect-diagnostics globalDate:off -- UI snooze presets use local calendar boundaries and Intl labels. |
| 2 | import type { OrchestrationThreadShell, SidebarAutoSettleMode } from "@t3tools/contracts"; | 2 | import type { OrchestrationThreadShell } from "@t3tools/contracts"; |
| 3 | 3 | ||
| 4 | export type ChangeRequestStateLike = "open" | "closed" | "merged"; | 4 | export type ChangeRequestStateLike = "open" | "closed" | "merged"; |
| 5 | 5 | ||
| ... | @@ -46,22 +46,26 @@ function threadUserActivityAnchorAt(thread: ThreadActivitySource): string { | ... | @@ -46,22 +46,26 @@ function threadUserActivityAnchorAt(thread: ThreadActivitySource): string { |
| 46 | * thread started at a worktree root whose PR already merged), and one older | 46 | * thread started at a worktree root whose PR already merged), and one older |
| 47 | * than the user's latest engagement was already adjudicated — re-engaging a | 47 | * than the user's latest engagement was already adjudicated — re-engaging a |
| 48 | * thread whose PR merged is the user saying the conversation outlived the | 48 | * thread whose PR merged is the user saying the conversation outlived the |
| 49 | * PR. Missing or malformed timestamps keep the thread active: an automatic | 49 | * PR. Unknown timestamps keep the old always-settle behavior. |
| 50 | * transition must have enough evidence to be predictable. | ||
| 51 | */ | 50 | */ |
| 52 | export function changeRequestAutoSettles( | 51 | export function changeRequestAutoSettles( |
| 53 | changeRequest: ChangeRequestSettleSource | null | undefined, | 52 | changeRequest: ChangeRequestSettleSource | null | undefined, |
| 54 | options: { | 53 | options: { |
| 54 | readonly autoSettleOnMerge?: boolean | undefined; | ||
| 55 | readonly thread?: ThreadActivitySource | null | undefined; | 55 | readonly thread?: ThreadActivitySource | null | undefined; |
| 56 | } = {}, | 56 | } = {}, |
| 57 | ): boolean { | 57 | ): boolean { |
| 58 | if (changeRequest == null) return false; | 58 | if (changeRequest == null) return false; |
| 59 | const terminal = changeRequest.state === "closed" || changeRequest.state === "merged"; | 59 | const terminal = |
| 60 | changeRequest.state === "closed" || | ||
| 61 | (changeRequest.state === "merged" && options.autoSettleOnMerge !== false); | ||
| 60 | if (!terminal) return false; | 62 | if (!terminal) return false; |
| 61 | if (changeRequest.updatedAt == null || options.thread == null) return false; | 63 | if (changeRequest.updatedAt == null || options.thread == null) return true; |
| 62 | const updatedAtMs = Date.parse(changeRequest.updatedAt); | 64 | const updatedAtMs = Date.parse(changeRequest.updatedAt); |
| 63 | const anchorAtMs = Date.parse(threadUserActivityAnchorAt(options.thread)); | 65 | const anchorAtMs = Date.parse(threadUserActivityAnchorAt(options.thread)); |
| 64 | if (Number.isNaN(updatedAtMs) || Number.isNaN(anchorAtMs)) return false; | 66 | // Malformed timestamps fall back to settling, matching servers that never |
| 67 | // report updatedAt. | ||
| 68 | if (Number.isNaN(updatedAtMs) || Number.isNaN(anchorAtMs)) return true; | ||
| 65 | return updatedAtMs >= anchorAtMs; | 69 | return updatedAtMs >= anchorAtMs; |
| 66 | } | 70 | } |
| 67 | 71 | ||
| ... | @@ -285,11 +289,11 @@ export function threadWokeAt( | ... | @@ -285,11 +289,11 @@ export function threadWokeAt( |
| 285 | * queued turn) are checked first and hold a thread active regardless of any | 289 | * queued turn) are checked first and hold a thread active regardless of any |
| 286 | * override. Past the blockers, the explicit user override (thread.settle / | 290 | * override. Past the blockers, the explicit user override (thread.settle / |
| 287 | * thread.unsettle commands, projected into settledOverride + settledAt) | 291 | * thread.unsettle commands, projected into settledOverride + settledAt) |
| 288 | * wins in both directions. Without one, exactly one selected policy can | 292 | * wins in both directions; without one, a thread can auto-settle on a |
| 289 | * auto-settle: a merged/closed pull request whose terminal timestamp is the | 293 | * merged PR or always on a closed PR (both only while the terminal state is |
| 290 | * thread's latest user event (see changeRequestAutoSettles), inactivity past | 294 | * the thread's latest event, see changeRequestAutoSettles), or settles on |
| 291 | * the configured window, or never. An open PR blocks the inactivity path. | 295 | * inactivity past the window. |
| 292 | * The server | 296 | * An open PR blocks the inactivity path entirely. The server |
| 293 | * un-settles on real activity (user message, session start, approval/ | 297 | * un-settles on real activity (user message, session start, approval/ |
| 294 | * user-input request), so an override never goes stale silently. | 298 | * user-input request), so an override never goes stale silently. |
| 295 | */ | 299 | */ |
| ... | @@ -298,7 +302,7 @@ export function effectiveSettled( | ... | @@ -298,7 +302,7 @@ export function effectiveSettled( |
| 298 | options: { | 302 | options: { |
| 299 | readonly now: string; | 303 | readonly now: string; |
| 300 | readonly autoSettleAfterDays: number | null; | 304 | readonly autoSettleAfterDays: number | null; |
| 301 | readonly autoSettleMode: SidebarAutoSettleMode; | 305 | readonly autoSettleOnMerge?: boolean; |
| 302 | readonly changeRequest?: ChangeRequestSettleSource | null; | 306 | readonly changeRequest?: ChangeRequestSettleSource | null; |
| 303 | }, | 307 | }, |
| 304 | ): boolean { | 308 | ): boolean { |
| ... | @@ -325,14 +329,18 @@ export function effectiveSettled( | ... | @@ -325,14 +329,18 @@ export function effectiveSettled( |
| 325 | // "active" is the explicit keep-active pin: it suppresses auto-settle | 329 | // "active" is the explicit keep-active pin: it suppresses auto-settle |
| 326 | // until real activity clears it server-side. | 330 | // until real activity clears it server-side. |
| 327 | if (shell.settledOverride === "active") return false; | 331 | if (shell.settledOverride === "active") return false; |
| 328 | if (options.autoSettleMode === "never") return false; | 332 | if ( |
| 329 | if (options.autoSettleMode === "change-request") { | 333 | changeRequestAutoSettles(options.changeRequest, { |
| 330 | return changeRequestAutoSettles(options.changeRequest, { thread: shell }); | 334 | autoSettleOnMerge: options.autoSettleOnMerge, |
| 335 | thread: shell, | ||
| 336 | }) | ||
| 337 | ) { | ||
| 338 | return true; | ||
| 331 | } | 339 | } |
| 332 | // An open PR is unfinished business regardless of how long the thread has | 340 | // An open PR is unfinished business regardless of how long the thread has |
| 333 | // been quiet: review can take days, and hiding the thread would bury the | 341 | // been quiet: review can take days, and hiding the thread would bury the |
| 334 | // work waiting on it. Only an explicit user settle resolves it while the | 342 | // work waiting on it. A configured merge, a close, or an explicit user |
| 335 | // inactivity policy is selected. | 343 | // settle resolves it. |
| 336 | if (options.changeRequest?.state === "open") return false; | 344 | if (options.changeRequest?.state === "open") return false; |
| 337 | if (options.autoSettleAfterDays === null) return false; | 345 | if (options.autoSettleAfterDays === null) return false; |
| 338 | 346 |
packages/contracts/src/settings.test.ts+9-23| ... | @@ -117,11 +117,11 @@ describe("ClientSettings environment identification", () => { | ... | @@ -117,11 +117,11 @@ describe("ClientSettings environment identification", () => { |
| 117 | }); | 117 | }); |
| 118 | 118 | ||
| 119 | describe("ClientSettings sidebar", () => { | 119 | describe("ClientSettings sidebar", () => { |
| 120 | it("defaults to the current sidebar with manual settling", () => { | 120 | it("defaults to the current sidebar with automatic merge and inactivity settling", () => { |
| 121 | const settings = decodeClientSettings({}); | 121 | const settings = decodeClientSettings({}); |
| 122 | expect(settings.legacySidebarEnabled).toBe(false); | 122 | expect(settings.legacySidebarEnabled).toBe(false); |
| 123 | expect(settings.sidebarAutoSettleAfterDays).toBe(3); | 123 | expect(settings.sidebarAutoSettleAfterDays).toBe(3); |
| 124 | expect(settings.sidebarAutoSettleMode).toBe("never"); | 124 | expect(settings.sidebarAutoSettleOnMerge).toBe(true); |
| 125 | }); | 125 | }); |
| 126 | 126 | ||
| 127 | it("drops the retired sidebar v2 beta keys, resetting everyone to the default", () => { | 127 | it("drops the retired sidebar v2 beta keys, resetting everyone to the default", () => { |
| ... | @@ -153,27 +153,13 @@ describe("ClientSettings sidebar", () => { | ... | @@ -153,27 +153,13 @@ describe("ClientSettings sidebar", () => { |
| 153 | ).toBeNull(); | 153 | ).toBeNull(); |
| 154 | }); | 154 | }); |
| 155 | 155 | ||
| 156 | it("does not preserve the retired merge switch as a hidden automatic path", () => { | 156 | it("allows auto-settle on merge to be disabled", () => { |
| 157 | const decoded = decodeClientSettings({ sidebarAutoSettleOnMerge: true }); | 157 | expect(decodeClientSettings({ sidebarAutoSettleOnMerge: false }).sidebarAutoSettleOnMerge).toBe( |
| 158 | expect(decoded.sidebarAutoSettleMode).toBe("never"); | 158 | false, |
| 159 | expect(decoded).not.toHaveProperty("sidebarAutoSettleOnMerge"); | 159 | ); |
| 160 | }); | 160 | expect( |
| 161 | 161 | decodeClientSettingsPatch({ sidebarAutoSettleOnMerge: false }).sidebarAutoSettleOnMerge, | |
| 162 | it.each(["never", "change-request", "inactivity"] as const)( | 162 | ).toBe(false); |
| 163 | "accepts the %s auto-settle mode", | ||
| 164 | (sidebarAutoSettleMode) => { | ||
| 165 | expect(decodeClientSettings({ sidebarAutoSettleMode }).sidebarAutoSettleMode).toBe( | ||
| 166 | sidebarAutoSettleMode, | ||
| 167 | ); | ||
| 168 | expect(decodeClientSettingsPatch({ sidebarAutoSettleMode }).sidebarAutoSettleMode).toBe( | ||
| 169 | sidebarAutoSettleMode, | ||
| 170 | ); | ||
| 171 | }, | ||
| 172 | ); | ||
| 173 | |||
| 174 | it("rejects unsupported auto-settle modes", () => { | ||
| 175 | expect(() => decodeClientSettings({ sidebarAutoSettleMode: "always" })).toThrow(); | ||
| 176 | expect(() => decodeClientSettingsPatch({ sidebarAutoSettleMode: "always" })).toThrow(); | ||
| 177 | }); | 163 | }); |
| 178 | 164 | ||
| 179 | it.each([-1, 0, 91])("rejects an auto-settle threshold outside 1..90: %s", (value) => { | 165 | it.each([-1, 0, 91])("rejects an auto-settle threshold outside 1..90: %s", (value) => { |
packages/contracts/src/settings.ts+2-10| ... | @@ -65,9 +65,6 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check( | ... | @@ -65,9 +65,6 @@ export const SidebarAutoSettleAfterDays = Schema.Number.check( |
| 65 | ); | 65 | ); |
| 66 | export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type; | 66 | export type SidebarAutoSettleAfterDays = typeof SidebarAutoSettleAfterDays.Type; |
| 67 | export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3; | 67 | export const DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS: SidebarAutoSettleAfterDays = 3; |
| 68 | export const SidebarAutoSettleMode = Schema.Literals(["never", "change-request", "inactivity"]); | ||
| 69 | export type SidebarAutoSettleMode = typeof SidebarAutoSettleMode.Type; | ||
| 70 | export const DEFAULT_SIDEBAR_AUTO_SETTLE_MODE: SidebarAutoSettleMode = "never"; | ||
| 71 | export const MIN_GLASS_OPACITY = 40; | 68 | export const MIN_GLASS_OPACITY = 40; |
| 72 | export const MAX_GLASS_OPACITY = 100; | 69 | export const MAX_GLASS_OPACITY = 100; |
| 73 | export const GlassOpacity = Schema.Int.check( | 70 | export const GlassOpacity = Schema.Int.check( |
| ... | @@ -238,12 +235,7 @@ export const ClientSettingsSchema = Schema.Struct({ | ... | @@ -238,12 +235,7 @@ export const ClientSettingsSchema = Schema.Struct({ |
| 238 | sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe( | 235 | sidebarAutoSettleAfterDays: Schema.NullOr(SidebarAutoSettleAfterDays).pipe( |
| 239 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)), | 236 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS)), |
| 240 | ), | 237 | ), |
| 241 | // A fresh exclusive policy replaces the old independent inactivity and | 238 | sidebarAutoSettleOnMerge: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), |
| 242 | // merge switches. Missing mode means manual-only, so an upgrade cannot | ||
| 243 | // preserve one hidden automatic path after the user disabled another. | ||
| 244 | sidebarAutoSettleMode: SidebarAutoSettleMode.pipe( | ||
| 245 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_AUTO_SETTLE_MODE)), | ||
| 246 | ), | ||
| 247 | sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( | 239 | sidebarProjectGroupingMode: SidebarProjectGroupingMode.pipe( |
| 248 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), | 240 | Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE)), |
| 249 | ), | 241 | ), |
| ... | @@ -952,7 +944,7 @@ export const ClientSettingsPatch = Schema.Struct({ | ... | @@ -952,7 +944,7 @@ export const ClientSettingsPatch = Schema.Struct({ |
| 952 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), | 944 | showSkillsInSlashMenu: Schema.optionalKey(Schema.Boolean), |
| 953 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), | 945 | legacySidebarEnabled: Schema.optionalKey(Schema.Boolean), |
| 954 | sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)), | 946 | sidebarAutoSettleAfterDays: Schema.optionalKey(Schema.NullOr(SidebarAutoSettleAfterDays)), |
| 955 | sidebarAutoSettleMode: Schema.optionalKey(SidebarAutoSettleMode), | 947 | sidebarAutoSettleOnMerge: Schema.optionalKey(Schema.Boolean), |
| 956 | sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), | 948 | sidebarProjectGroupingMode: Schema.optionalKey(SidebarProjectGroupingMode), |
| 957 | sidebarProjectGroupingOverrides: Schema.optionalKey( | 949 | sidebarProjectGroupingOverrides: Schema.optionalKey( |
| 958 | Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), | 950 | Schema.Record(TrimmedNonEmptyString, SidebarProjectGroupingMode), |