Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const clientSettings: ClientSettings = {
glassOpacity: 80,
providerModelPreferences: {},
sidebarAutoSettleAfterDays: 3,
sidebarAutoSettleOnChangeRequestCompletion: true,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down Expand Up @@ -153,6 +154,7 @@ describe("DesktopClientSettings", () => {
assert.isTrue(Option.isSome(persisted));
if (Option.isSome(persisted)) {
assert.equal(persisted.value.timestampFormat, "24-hour");
assert.isTrue(persisted.value.sidebarAutoSettleOnChangeRequestCompletion);
}
}),
),
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ import { scopedProjectKey } from "../../lib/scopedEntities";
import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { useThreadSearch } from "../../state/queries";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
import {
useThreadListV2Enabled,
useThreadListV2SettlementPreferences,
} from "../threads/use-thread-list-v2-enabled";
import { environmentServerConfigsAtom } from "../../state/server";
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
import {
Expand Down Expand Up @@ -202,6 +205,7 @@ export function HomeScreen(props: HomeScreenProps) {
>(() => new Map());
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const threadListV2Enabled = useThreadListV2Enabled();
const threadListV2SettlementPreferences = useThreadListV2SettlementPreferences();
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
const openSwipeableRef = useRef<SwipeableMethods | null>(null);
const listRef = useRef<LegendListRef | null>(null);
Expand Down Expand Up @@ -597,6 +601,7 @@ export function HomeScreen(props: HomeScreenProps) {
changeRequestStateByKey,
settlementEnvironmentIds,
snoozeEnvironmentIds,
...threadListV2SettlementPreferences,
settledLimit: settledVisibleCount,
now: `${nowMinute}:00.000Z`,
snoozeNow: new Date().toISOString(),
Expand All @@ -613,6 +618,7 @@ export function HomeScreen(props: HomeScreenProps) {
settledVisibleCount,
settlementEnvironmentIds,
snoozeEnvironmentIds,
threadListV2SettlementPreferences,
props.searchQuery,
props.selectedEnvironmentId,
props.threads,
Expand Down
30 changes: 28 additions & 2 deletions apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
import { runtime } from "../../lib/runtime";
import { useThemeColor } from "../../lib/useThemeColor";
import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
import {
useThreadListV2Enabled,
useThreadListV2SettlementPreferences,
} from "../threads/use-thread-list-v2-enabled";
import {
type AppUpdateCheckState,
registerHiddenUpdateTap,
Expand Down Expand Up @@ -555,6 +558,8 @@ function GeneralSettingsSection() {
function BetaSettingsSection() {
const savePreferences = useAtomSet(updateMobilePreferencesAtom);
const threadListV2Enabled = useThreadListV2Enabled();
const { autoSettleAfterDays, autoSettleOnChangeRequestCompletion } =
useThreadListV2SettlementPreferences();

return (
<View className="gap-3">
Expand All @@ -565,10 +570,31 @@ function BetaSettingsSection() {
value={threadListV2Enabled}
onValueChange={(value) => savePreferences({ threadListV2Enabled: value })}
/>
{threadListV2Enabled ? (
<>
<SettingsSwitchRow
icon="clock"
label="Auto-settle inactive threads"
value={autoSettleAfterDays !== null}
onValueChange={(value) => savePreferences({ threadListV2AutoSettleInactive: value })}
/>
<SettingsSwitchRow
icon="arrow.triangle.pull"
label="Auto-settle merged/closed PRs"
value={autoSettleOnChangeRequestCompletion}
onValueChange={(value) =>
savePreferences({
threadListV2AutoSettleOnChangeRequestCompletion: value,
})
}
/>
</>
) : null}
</SettingsSection>
<Text className="px-2 text-sm text-foreground-muted">
One flat thread list in creation order. Active work renders as cards; settled threads
collapse to compact rows. Switch back any time.
collapse to compact rows. Inactive threads use a three-day window when automatic settling is
on. Switch back any time.
</Text>
</View>
);
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
import { useThemeColor } from "../../lib/useThemeColor";
import { useProjects, useThreadShells } from "../../state/entities";
import { useThreadSearch } from "../../state/queries";
import { useThreadListV2Enabled } from "./use-thread-list-v2-enabled";
import {
useThreadListV2Enabled,
useThreadListV2SettlementPreferences,
} from "./use-thread-list-v2-enabled";
import { environmentServerConfigsAtom } from "../../state/server";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
import { useWorkspaceState } from "../../state/workspace";
Expand Down Expand Up @@ -207,6 +210,7 @@ function ThreadNavigationSidebarPane(
unsettleThread,
} = useThreadListActions();
const threadListV2Enabled = useThreadListV2Enabled();
const threadListV2SettlementPreferences = useThreadListV2SettlementPreferences();
const pendingTasks = usePendingNewTasks();
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(
Expand Down Expand Up @@ -501,6 +505,7 @@ function ThreadNavigationSidebarPane(
changeRequestStateByKey,
settlementEnvironmentIds,
snoozeEnvironmentIds,
...threadListV2SettlementPreferences,
settledLimit: settledVisibleCount,
now: `${nowMinute}:00.000Z`,
snoozeNow: new Date().toISOString(),
Expand All @@ -521,6 +526,7 @@ function ThreadNavigationSidebarPane(
settledVisibleCount,
settlementEnvironmentIds,
snoozeEnvironmentIds,
threadListV2SettlementPreferences,
threadListV2Enabled,
threads,
selectedProjectScope,
Expand Down
51 changes: 51 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
buildThreadListV2Items,
buildThreadListV2ListItems,
resolveThreadListV2Enabled,
resolveThreadListV2SettlementPreferences,
resolveThreadListV2SnoozeMenuSelection,
resolveThreadListV2SnoozeGateExpiryMs,
resolveThreadListV2Status,
Expand Down Expand Up @@ -121,6 +122,35 @@ describe("resolveThreadListV2Enabled", () => {
});
});

describe("resolveThreadListV2SettlementPreferences", () => {
it("defaults both automatic triggers on", () => {
expect(resolveThreadListV2SettlementPreferences({ preferencesLoaded: true })).toEqual({
autoSettleAfterDays: 3,
autoSettleOnChangeRequestCompletion: true,
});
});

it("holds both automatic triggers off while preferences are loading", () => {
expect(resolveThreadListV2SettlementPreferences({ preferencesLoaded: false })).toEqual({
autoSettleAfterDays: null,
autoSettleOnChangeRequestCompletion: false,
});
});

it("supports fully manual settlement", () => {
expect(
resolveThreadListV2SettlementPreferences({
autoSettleInactive: false,
autoSettleOnChangeRequestCompletion: false,
preferencesLoaded: true,
}),
).toEqual({
autoSettleAfterDays: null,
autoSettleOnChangeRequestCompletion: false,
});
});
});

describe("resolveThreadListV2Status", () => {
it("prioritizes approval over a running session", () => {
const thread = makeThread({
Expand Down Expand Up @@ -259,6 +289,27 @@ describe("sortThreadsForListV2", () => {
});

describe("buildThreadListV2Items", () => {
it("keeps a stale merged-review thread active when both auto-settle triggers are disabled", () => {
const thread = makeThread({
id: ThreadId.make("manual-only"),
title: "Manual only",
latestUserMessageAt: "2026-05-01T00:00:00.000Z",
});
const layout = buildThreadListV2Items({
threads: [thread],
environmentId: null,
searchQuery: "",
changeRequestStateByKey: new Map([[`${environmentId}:${thread.id}`, "merged"]]),
autoSettleAfterDays: null,
autoSettleOnChangeRequestCompletion: false,
now: NOW,
});

expect(layout.items.map((item) => [item.thread.id, item.variant])).toEqual([
["manual-only", "card"],
]);
});

it("hides snoozed threads and counts them — visibility parity with web", () => {
const layout = buildThreadListV2Items({
threads: [
Expand Down
46 changes: 39 additions & 7 deletions apps/mobile/src/features/threads/threadListV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
import type { SnoozePreset } from "@t3tools/client-runtime/state/thread-settled";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search";
import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
import {
DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
type EnvironmentId,
type ProjectId,
} from "@t3tools/contracts";

import type { PendingNewTask } from "../../state/use-pending-new-tasks";

Expand All @@ -26,6 +30,24 @@ export { snoozeWakeLabel };
export type ThreadListV2Status = "approval" | "input" | "working" | "failed" | "ready";
export type ThreadListV2SwipeAction = "archive" | "settle" | "unsettle" | "snooze" | "unsnooze";

export function resolveThreadListV2SettlementPreferences(input: {
readonly autoSettleInactive?: boolean;
readonly autoSettleOnChangeRequestCompletion?: boolean;
readonly preferencesLoaded: boolean;
}) {
if (!input.preferencesLoaded) {
return {
autoSettleAfterDays: null,
autoSettleOnChangeRequestCompletion: false,
};
}
return {
autoSettleAfterDays:
input.autoSettleInactive === false ? null : DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
autoSettleOnChangeRequestCompletion: input.autoSettleOnChangeRequestCompletion !== false,
};
}

export function resolveThreadListV2SnoozeMenuSelection(input: {
readonly event: string;
readonly displayedPresets: ReadonlyArray<SnoozePreset>;
Expand Down Expand Up @@ -303,9 +325,9 @@ export function buildThreadListV2ListItems(input: {

/**
* Partitions visible threads into the active card block (creation order) and
* the settled recency tail, matching the web v2 list. `autoSettleAfterDays`
* mirrors the web default of 3 — mobile has no client-settings sync yet, so
* the default is fixed here rather than user-configurable.
* the settled recency tail, matching the web v2 list. Automatic-settlement
* preferences are device-local on mobile and default to the existing
* three-day inactivity window plus terminal-review settlement.
*/
export function buildThreadListV2Items(input: {
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
Expand All @@ -325,7 +347,8 @@ export function buildThreadListV2Items(input: {
/** Environments whose server supports thread.snooze/unsnooze. Same
contract as settlementEnvironmentIds. */
readonly snoozeEnvironmentIds?: ReadonlySet<EnvironmentId>;
readonly autoSettleAfterDays?: number;
readonly autoSettleAfterDays?: number | null;
readonly autoSettleOnChangeRequestCompletion?: boolean;
/** Max settled rows to render; the rest are counted, not built. */
readonly settledLimit?: number;
/** Injectable for tests; defaults to now. */
Expand All @@ -345,7 +368,11 @@ export function buildThreadListV2Items(input: {
}): ThreadListV2Layout {
const now = input.now ?? new Date().toISOString();
const snoozeNow = input.snoozeNow ?? now;
const autoSettleAfterDays = input.autoSettleAfterDays ?? 3;
const autoSettleAfterDays =
input.autoSettleAfterDays === undefined
? DEFAULT_SIDEBAR_AUTO_SETTLE_AFTER_DAYS
: input.autoSettleAfterDays;
const autoSettleOnChangeRequestCompletion = input.autoSettleOnChangeRequestCompletion !== false;
const query = input.searchQuery.trim().toLocaleLowerCase();
const projectKeys = input.projectRefs
? new Set(input.projectRefs.map((ref) => `${ref.environmentId}:${ref.projectId}`))
Expand Down Expand Up @@ -394,7 +421,12 @@ export function buildThreadListV2Items(input: {
}
if (
supportsSettlement &&
effectiveSettled(thread, { now, autoSettleAfterDays, changeRequestState })
effectiveSettled(thread, {
now,
autoSettleAfterDays,
autoSettleOnChangeRequestCompletion,
changeRequestState,
})
) {
settled.push(thread);
} else {
Expand Down
24 changes: 23 additions & 1 deletion apps/mobile/src/features/threads/use-thread-list-v2-enabled.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { useAtomValue } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import { useMemo } from "react";

import { mobilePreferencesAtom } from "../../state/preferences";
import { resolveThreadListV2Enabled } from "./threadListV2";
import {
resolveThreadListV2Enabled,
resolveThreadListV2SettlementPreferences,
} from "./threadListV2";

/**
* Resolved Thread List v2 state: the device-local preference if the user has
Expand All @@ -17,3 +21,21 @@ export function useThreadListV2Enabled(): boolean {
preferencesLoaded: loaded,
});
}

export function useThreadListV2SettlementPreferences() {
const preferencesResult = useAtomValue(mobilePreferencesAtom);
const preferencesLoaded = AsyncResult.isSuccess(preferencesResult);
const preferences = preferencesLoaded ? preferencesResult.value : undefined;
const autoSettleInactive = preferences?.threadListV2AutoSettleInactive;
const autoSettleOnChangeRequestCompletion =
preferences?.threadListV2AutoSettleOnChangeRequestCompletion;
return useMemo(
() =>
resolveThreadListV2SettlementPreferences({
autoSettleInactive,
autoSettleOnChangeRequestCompletion,
preferencesLoaded,
}),
[autoSettleInactive, autoSettleOnChangeRequestCompletion, preferencesLoaded],
);
Comment thread
cursor[bot] marked this conversation as resolved.
}
12 changes: 12 additions & 0 deletions apps/mobile/src/persistence/mobile-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export interface Preferences {
* see `resolveThreadListV2Enabled`.
*/
readonly threadListV2Enabled?: boolean;
/** Device-local counterparts of the web Sidebar v2 auto-settle settings. */
readonly threadListV2AutoSettleInactive?: boolean;
readonly threadListV2AutoSettleOnChangeRequestCompletion?: boolean;
}

export class MobilePreferencesLoadError extends Schema.TaggedErrorClass<MobilePreferencesLoadError>()(
Expand Down Expand Up @@ -81,6 +84,8 @@ function sanitizePreferences(parsed: Preferences): Preferences {
collapsedProjectGroups?: readonly string[];
projectGroupingEnabled?: boolean;
threadListV2Enabled?: boolean;
threadListV2AutoSettleInactive?: boolean;
threadListV2AutoSettleOnChangeRequestCompletion?: boolean;
} = {};

if (typeof parsed.liveActivitiesEnabled === "boolean") {
Expand Down Expand Up @@ -113,6 +118,13 @@ function sanitizePreferences(parsed: Preferences): Preferences {
if (typeof parsed.threadListV2Enabled === "boolean") {
preferences.threadListV2Enabled = parsed.threadListV2Enabled;
}
if (typeof parsed.threadListV2AutoSettleInactive === "boolean") {
preferences.threadListV2AutoSettleInactive = parsed.threadListV2AutoSettleInactive;
}
if (typeof parsed.threadListV2AutoSettleOnChangeRequestCompletion === "boolean") {
preferences.threadListV2AutoSettleOnChangeRequestCompletion =
parsed.threadListV2AutoSettleOnChangeRequestCompletion;
}
return preferences;
}

Expand Down
Loading
Loading