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
12 changes: 12 additions & 0 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,16 @@ export function HomeScreen(props: HomeScreenProps) {
}
return supported;
}, [serverConfigs]);
const autoSettleAfterDaysByEnvironment = useMemo(
() =>
new Map(
[...serverConfigs].map(
([environmentId, config]) =>
[environmentId, config.settings.threadSettlement.autoSettleAfterDays] as const,
),
),
[serverConfigs],
);
const snoozeEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
Expand Down Expand Up @@ -596,6 +606,7 @@ export function HomeScreen(props: HomeScreenProps) {
matchedThreadKeys,
changeRequestStateByKey,
settlementEnvironmentIds,
autoSettleAfterDaysByEnvironment,
snoozeEnvironmentIds,
settledLimit: settledVisibleCount,
now: `${nowMinute}:00.000Z`,
Expand All @@ -612,6 +623,7 @@ export function HomeScreen(props: HomeScreenProps) {
settledShelfExpanded,
settledVisibleCount,
settlementEnvironmentIds,
autoSettleAfterDaysByEnvironment,
snoozeEnvironmentIds,
props.searchQuery,
props.selectedEnvironmentId,
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,16 @@ function ThreadNavigationSidebarPane(
}
return supported;
}, [serverConfigs]);
const autoSettleAfterDaysByEnvironment = useMemo(
() =>
new Map(
[...serverConfigs].map(
([environmentId, config]) =>
[environmentId, config.settings.threadSettlement.autoSettleAfterDays] as const,
),
),
[serverConfigs],
);
const snoozeEnvironmentIds = useMemo(() => {
const supported = new Set<EnvironmentId>();
for (const [environmentId, config] of serverConfigs) {
Expand Down Expand Up @@ -500,6 +510,7 @@ function ThreadNavigationSidebarPane(
matchedThreadKeys,
changeRequestStateByKey,
settlementEnvironmentIds,
autoSettleAfterDaysByEnvironment,
snoozeEnvironmentIds,
settledLimit: settledVisibleCount,
now: `${nowMinute}:00.000Z`,
Expand All @@ -520,6 +531,7 @@ function ThreadNavigationSidebarPane(
matchedThreadKeys,
settledVisibleCount,
settlementEnvironmentIds,
autoSettleAfterDaysByEnvironment,
snoozeEnvironmentIds,
threadListV2Enabled,
threads,
Expand Down
64 changes: 64 additions & 0 deletions apps/mobile/src/features/threads/threadListV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,70 @@ describe("sortThreadsForListV2", () => {
});

describe("buildThreadListV2Items", () => {
it("uses each thread's environment policy without client drift", () => {
const threeDayEnvironment = EnvironmentId.make("environment-three-days");
const sevenDayEnvironment = EnvironmentId.make("environment-seven-days");
const disabledEnvironment = EnvironmentId.make("environment-disabled");
const missingEnvironment = EnvironmentId.make("environment-missing-policy");
const sameAge = {
createdAt: "2026-06-05T00:00:00.000Z",
updatedAt: "2026-06-05T00:00:00.000Z",
latestUserMessageAt: "2026-06-05T00:00:00.000Z",
latestTurn: {
turnId: TurnId.make("same-age-turn"),
state: "completed",
requestedAt: "2026-06-05T00:00:00.000Z",
startedAt: "2026-06-05T00:00:01.000Z",
completedAt: "2026-06-05T00:01:00.000Z",
assistantMessageId: null,
},
} as const;
const layout = buildThreadListV2Items({
threads: [
makeThread({
...sameAge,
environmentId: threeDayEnvironment,
id: ThreadId.make("three-days"),
title: "Three days",
}),
makeThread({
...sameAge,
environmentId: sevenDayEnvironment,
id: ThreadId.make("seven-days"),
title: "Seven days",
}),
makeThread({
...sameAge,
environmentId: disabledEnvironment,
id: ThreadId.make("disabled"),
title: "Disabled",
}),
makeThread({
...sameAge,
environmentId: missingEnvironment,
id: ThreadId.make("missing"),
title: "Missing",
}),
],
environmentId: null,
searchQuery: "",
autoSettleAfterDaysByEnvironment: new Map([
[threeDayEnvironment, 3],
[sevenDayEnvironment, 7],
[disabledEnvironment, null],
]),
now: "2026-06-10T00:00:00.000Z",
});

expect(Object.fromEntries(layout.items.map((item) => [item.thread.id, item.variant]))).toEqual({
disabled: "card",
"seven-days": "card",
missing: "slim",
"three-days": "slim",
});
expect(layout.settledCount).toBe(2);
});

it("hides snoozed threads and counts them — visibility parity with web", () => {
const layout = buildThreadListV2Items({
threads: [
Expand Down
18 changes: 12 additions & 6 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_THREAD_AUTO_SETTLE_AFTER_DAYS,
type EnvironmentId,
type ProjectId,
} from "@t3tools/contracts";

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

Expand Down Expand Up @@ -303,9 +307,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. Inactivity policy is
* resolved from each thread's owning environment. Missing policy data keeps
* the test-friendly and version-skew-compatible server default of three days.
*/
export function buildThreadListV2Items(input: {
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
Expand All @@ -325,7 +329,7 @@ export function buildThreadListV2Items(input: {
/** Environments whose server supports thread.snooze/unsnooze. Same
contract as settlementEnvironmentIds. */
readonly snoozeEnvironmentIds?: ReadonlySet<EnvironmentId>;
readonly autoSettleAfterDays?: number;
readonly autoSettleAfterDaysByEnvironment?: ReadonlyMap<EnvironmentId, number | null>;
/** Max settled rows to render; the rest are counted, not built. */
readonly settledLimit?: number;
/** Injectable for tests; defaults to now. */
Expand All @@ -345,7 +349,6 @@ export function buildThreadListV2Items(input: {
}): ThreadListV2Layout {
const now = input.now ?? new Date().toISOString();
const snoozeNow = input.snoozeNow ?? now;
const autoSettleAfterDays = input.autoSettleAfterDays ?? 3;
const query = input.searchQuery.trim().toLocaleLowerCase();
const projectKeys = input.projectRefs
? new Set(input.projectRefs.map((ref) => `${ref.environmentId}:${ref.projectId}`))
Expand Down Expand Up @@ -376,6 +379,9 @@ export function buildThreadListV2Items(input: {
}
const supportsSettlement = input.settlementEnvironmentIds?.has(thread.environmentId) ?? true;
const supportsSnooze = input.snoozeEnvironmentIds?.has(thread.environmentId) ?? true;
const autoSettleAfterDays = input.autoSettleAfterDaysByEnvironment?.has(thread.environmentId)
? (input.autoSettleAfterDaysByEnvironment.get(thread.environmentId) ?? null)
: DEFAULT_THREAD_AUTO_SETTLE_AFTER_DAYS;
const changeRequestState =
input.changeRequestStateByKey?.get(`${thread.environmentId}:${thread.id}`) ?? null;
// Visibility parity with web: a snoozed thread leaves the list until it
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const make = Effect.gen(function* () {
repositoryIdentity: true,
connectionProbe: true,
threadSettlement: true,
threadSettlementPolicy: true,
threadSnooze: true,
threadTitleRegeneration: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/serverSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ it.layer(NodeServices.layer)("server settings", (it) => {
}),
);

it.effect("persists thread settlement policy patches", () =>
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
const serverConfig = yield* ServerConfig.ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;

const updated = yield* serverSettings.updateSettings({
threadSettlement: { autoSettleAfterDays: 14 },
});
assert.deepEqual(updated.threadSettlement, { autoSettleAfterDays: 14 });
assert.include(
yield* fileSystem.readFileString(serverConfig.settingsPath),
'"threadSettlement": {',
);

const disabled = yield* serverSettings.updateSettings({
threadSettlement: { autoSettleAfterDays: null },
});
assert.isNull(disabled.threadSettlement.autoSettleAfterDays);
}).pipe(Effect.provide(makeServerSettingsLayer())),
);

it.effect(
"decodes legacy object-shaped textGenerationModelSelection.options from settings.json",
() =>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ import {
import { newDraftId, newMessageId, newThreadId } from "~/lib/utils";
import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels";
import { NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";
import { useClientSettings, useEnvironmentSettings } from "../hooks/useSettings";
import { useEnvironmentSettings } from "../hooks/useSettings";
import { useNowMinute } from "../hooks/useNowMinute";
import { useNewThreadHandler } from "../hooks/useHandleNewThread";
import { resolveAppModelSelectionForInstance } from "../modelSelection";
Expand Down Expand Up @@ -3961,7 +3961,7 @@ function ChatViewContent(props: ChatViewProps) {
// partition (same shell, same capability gate, same PR auto-settle input)
// so the banner and the sidebar row never disagree.
const activeThreadShell = useThreadShell(isServerThread ? activeThreadRef : null);
const autoSettleAfterDays = useClientSettings((settings) => settings.sidebarAutoSettleAfterDays);
const autoSettleAfterDays = settings.threadSettlement.autoSettleAfterDays;
const activeThreadPr = resolveThreadPr({
threadBranch: activeThread?.branch ?? null,
gitStatus: gitStatusQuery.data ?? null,
Expand Down
11 changes: 5 additions & 6 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,6 @@ export default function SidebarV2() {
const router = useRouter();
const { isMobile, setOpenMobile } = useSidebar();
const keybindings = useAtomValue(primaryServerKeybindingsAtom);
const autoSettleAfterDays = useClientSettings((s) => s.sidebarAutoSettleAfterDays);
const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete);
const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
Expand Down Expand Up @@ -1584,10 +1583,11 @@ export default function SidebarV2() {
// or descriptor not loaded yet) never classify as settled: the user
// could neither un-settle nor pin them, so auto-settling them would
// strand rows in a tail with no working affordances.
const supportsSettlement =
serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSettlement === true;
const supportsSnooze =
serverConfigs.get(thread.environmentId)?.environment.capabilities.threadSnooze === true;
const serverConfig = serverConfigs.get(thread.environmentId);
const supportsSettlement = serverConfig?.environment.capabilities.threadSettlement === true;
const supportsSnooze = serverConfig?.environment.capabilities.threadSnooze === true;
const autoSettleAfterDays =
serverConfig?.settings.threadSettlement.autoSettleAfterDays ?? null;
const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
const changeRequestState = changeRequestStateByKey.get(threadKey) ?? null;
// Snooze outranks settled classification: an explicitly snoozed thread
Expand Down Expand Up @@ -1616,7 +1616,6 @@ export default function SidebarV2() {
snoozeNow: preciseNow,
};
}, [
autoSettleAfterDays,
changeRequestStateByKey,
nowMinute,
scopedProjectKeys,
Expand Down
Loading
Loading