Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2b6a3c6
fix(server): preserve Codex titles on import
debugrror Sep 15, 2026
dcfd375
fix(server): harden Codex title repair
debugrror Sep 15, 2026
5f97436
feat(server): sync imported provider sessions
matthias-scale Sep 17, 2026
9ed3809
feat(settings): settle idle threads after hours, keep imports active
matthias-scale Sep 17, 2026
5fae461
Merge branch 'feat/import-provider-titles' into integ/titles-settle
matthias-scale Sep 17, 2026
7da2817
test(server): expect active imported threads
matthias-scale Sep 17, 2026
24745c3
feat(server): import sessions from additional Codex homes
matthias-scale Sep 17, 2026
aa388f9
fix(server): harden multi-home Codex session sync
matthias-scale Sep 17, 2026
22b7136
fix(server): sync marked imported thread titles
matthias-scale Sep 17, 2026
18a9a6f
chore(release): version fork build 0.0.43-fork.1
matthias-scale Sep 17, 2026
27d7d36
fix(server): wake settled threads on synced activity
matthias-scale Sep 17, 2026
479788a
chore(release): version fork build 0.0.43-fork.2
matthias-scale Sep 17, 2026
e7352e9
fix(settings): preserve auto-settle writes across versions
matthias-scale Sep 17, 2026
535461c
fix(server): repin imports to active Codex home
matthias-scale Sep 17, 2026
974eff7
fix(server): retain Herdr marker on title sync
matthias-scale Sep 17, 2026
144d171
fix(settings): encode legacy auto-settle days
matthias-scale Sep 17, 2026
decd7ab
fix(settings): gate auto-settle units by capability
matthias-scale Sep 17, 2026
9fa5fe2
docs: correct auto-settle default
matthias-scale Sep 17, 2026
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
69 changes: 54 additions & 15 deletions apps/mobile/src/features/settings/SettingsRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AutoSettleHoursField } from "./components/AutoSettleHoursField";
import { AutoSettleDaysField } from "./components/AutoSettleDaysField";
import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
import { useAuth, useUser } from "@clerk/expo";
Expand Down Expand Up @@ -44,7 +45,10 @@ import { serverEnvironment } from "../../state/server";
import { useAtomCommand } from "../../state/use-atom-command";
import { useEnvironments } from "../../state/environments";
import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts";
import { supportsSharedSettingsSync } from "@t3tools/client-runtime/state/shared-settings";
import {
selectAutoSettleThreshold,
supportsSharedSettingsSync,
} from "@t3tools/client-runtime/state/shared-settings";
import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
import {
type AppUpdateCheckState,
Expand All @@ -58,7 +62,11 @@ import { SettingsSection } from "./components/SettingsSection";
import { SettingsSwitchRow } from "./components/SettingsSwitchRow";
import { SettingsScreen } from "./components/SettingsScreen";
import { resolveAgentAwarenessPlatformPresentation } from "./SettingsRouteScreen.logic";
import { planAutoSettleSettingsSync, type AutoSettleSettings } from "./autoSettleSettingsSync";
import {
planAutoSettleSettingsSync,
planAutoSettleSettingsWrites,
type AutoSettleSettings,
} from "./autoSettleSettingsSync";

type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported";
type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking";
Expand Down Expand Up @@ -611,7 +619,8 @@ function GeneralSettingsSection() {
);
}

const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterDays ?? 3;
const AUTO_SETTLE_DEFAULT_HOURS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterHours ?? 12;
const AUTO_SETTLE_DEFAULT_DAYS = 3;

/**
* Mobile edits auto-settle defaults across connected, capable environments.
Expand All @@ -637,22 +646,41 @@ function AutoSettleSettingsRows() {
const writeToAll = (patch: Partial<AutoSettleSettings>) => {
setPendingWrites((count) => count + 1);
void Promise.allSettled(
syncTargets.map((environment) =>
updateSettings({ environmentId: environment.environmentId, input: { patch } }),
planAutoSettleSettingsWrites(
patch,
syncTargets.map((environment) => ({
environmentId: environment.environmentId,
capabilities: environment.serverConfig?.environment.capabilities,
})),
).map((write) =>
updateSettings({ environmentId: write.environmentId, input: { patch: write.patch } }),
),
).finally(() => setPendingWrites((count) => count - 1));
};

const { patch: autoSettlePatch, mismatches } = planAutoSettleSettingsSync(
{ environmentId: reference.environmentId, settings: referenceSettings },
{
environmentId: reference.environmentId,
settings: referenceSettings,
capabilities: reference.serverConfig?.environment.capabilities,
},
syncTargets.map((environment) => ({
environmentId: environment.environmentId,
label: environment.label,
settings: environment.serverConfig?.settings ?? null,
capabilities: environment.serverConfig?.environment.capabilities,
})),
);

const afterDays = referenceSettings.sidebarAutoSettleAfterDays;
const selectedThreshold = selectAutoSettleThreshold(
referenceSettings,
reference.serverConfig?.environment.capabilities,
);
const supportsHours = selectedThreshold.key === "sidebarAutoSettleAfterHours";
const threshold =
selectedThreshold.value === undefined ? AUTO_SETTLE_DEFAULT_DAYS : selectedThreshold.value;
const thresholdPatch = (value: number | null): Partial<AutoSettleSettings> =>
supportsHours ? { sidebarAutoSettleAfterHours: value } : { sidebarAutoSettleAfterDays: value };

return (
<>
Expand All @@ -665,12 +693,16 @@ function AutoSettleSettingsRows() {
<SettingsSwitchRow
icon="clock"
label="Auto-settle inactive threads"
value={afterDays !== null}
value={threshold !== null}
onValueChange={(value) =>
writeToAll({ sidebarAutoSettleAfterDays: value ? AUTO_SETTLE_DEFAULT_DAYS : null })
writeToAll(
thresholdPatch(
value ? (supportsHours ? AUTO_SETTLE_DEFAULT_HOURS : AUTO_SETTLE_DEFAULT_DAYS) : null,
),
)
}
/>
{afterDays !== null ? (
{threshold !== null ? (
<View
className={cn(
"flex-row items-center gap-4 px-4",
Expand All @@ -684,12 +716,19 @@ function AutoSettleSettingsRows() {
Platform.OS === "android" ? "text-base" : "text-lg",
)}
>
Inactive days
{supportsHours ? "Inactive hours" : "Inactive days"}
</Text>
<AutoSettleDaysField
value={afterDays}
onValueChange={(value) => writeToAll({ sidebarAutoSettleAfterDays: value })}
/>
{supportsHours ? (
<AutoSettleHoursField
value={threshold}
onValueChange={(value) => writeToAll(thresholdPatch(value))}
/>
) : (
<AutoSettleDaysField
value={threshold}
onValueChange={(value) => writeToAll(thresholdPatch(value))}
/>
)}
</View>
) : null}
{pendingWrites === 0 && mismatches.length > 0 ? (
Expand Down
80 changes: 74 additions & 6 deletions apps/mobile/src/features/settings/autoSettleSettingsSync.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,86 @@
import { DEFAULT_SERVER_SETTINGS, EnvironmentId } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { planAutoSettleSettingsSync } from "./autoSettleSettingsSync";
import { planAutoSettleSettingsSync, planAutoSettleSettingsWrites } from "./autoSettleSettingsSync";

const reference = {
environmentId: EnvironmentId.make("reference"),
capabilities: { threadAutoSettlementHours: true },
settings: {
...DEFAULT_SERVER_SETTINGS,
sidebarAutoSettleAfterDays: 7,
sidebarAutoSettleAfterHours: 7,
sidebarAutoSettleOnMerge: true,
newWorktreesStartFromOrigin: false,
continueThreadsAfterServerUpdate: false,
},
};

describe("auto-settle settings sync", () => {
it("writes only the auto-settle unit advertised by each target", () => {
const legacyId = EnvironmentId.make("legacy");
const currentId = EnvironmentId.make("current");

expect(
planAutoSettleSettingsWrites(
{ sidebarAutoSettleAfterHours: 25, sidebarAutoSettleAfterDays: 3 },
[
{ environmentId: legacyId, capabilities: {} },
{
environmentId: currentId,
capabilities: { threadAutoSettlementHours: true },
},
],
),
).toEqual([
{ environmentId: legacyId, patch: { sidebarAutoSettleAfterDays: 3 } },
{ environmentId: currentId, patch: { sidebarAutoSettleAfterHours: 25 } },
]);
});

it("preserves a disabled legacy inactivity threshold without adding hours", () => {
expect(
planAutoSettleSettingsWrites({ sidebarAutoSettleAfterDays: null }, [
{ environmentId: EnvironmentId.make("legacy") },
])[0]?.patch,
).toEqual({ sidebarAutoSettleAfterDays: null });
});

it("does not send empty writes to targets using the opposite unit", () => {
const legacyId = EnvironmentId.make("legacy");
const currentId = EnvironmentId.make("current");

expect(
planAutoSettleSettingsWrites({ sidebarAutoSettleAfterHours: 25 }, [
{ environmentId: legacyId },
{
environmentId: currentId,
capabilities: { threadAutoSettlementHours: true },
},
]),
).toEqual([{ environmentId: currentId, patch: { sidebarAutoSettleAfterHours: 25 } }]);
});

it("reads a legacy reference in days without converting its value", () => {
const legacyReference = {
environmentId: EnvironmentId.make("legacy"),
capabilities: {},
settings: {
...reference.settings,
sidebarAutoSettleAfterDays: 3,
},
};

expect(planAutoSettleSettingsSync(legacyReference, []).patch).toEqual({
sidebarAutoSettleAfterDays: 3,
sidebarAutoSettleOnMerge: true,
});
});

it("ignores differences in independently configured environment settings", () => {
const target = {
environmentId: EnvironmentId.make("remote"),
label: "Remote",
capabilities: reference.capabilities,
settings: {
...reference.settings,
newWorktreesStartFromOrigin: true,
Expand All @@ -34,7 +96,7 @@ describe("auto-settle settings sync", () => {

expect(plan.mismatches).toEqual([]);
expect(plan.patch).toEqual({
sidebarAutoSettleAfterDays: 7,
sidebarAutoSettleAfterHours: 7,
sidebarAutoSettleOnMerge: true,
});
});
Expand All @@ -43,9 +105,10 @@ describe("auto-settle settings sync", () => {
const target = {
environmentId: EnvironmentId.make("remote"),
label: "Remote",
capabilities: reference.capabilities,
settings: {
...reference.settings,
sidebarAutoSettleAfterDays: null,
sidebarAutoSettleAfterHours: null,
sidebarAutoSettleOnMerge: false,
newWorktreesStartFromOrigin: true,
continueThreadsAfterServerUpdate: true,
Expand All @@ -60,7 +123,7 @@ describe("auto-settle settings sync", () => {
const updated = { ...target.settings, ...plan.patch };

expect(plan.mismatches).toEqual([target]);
expect(updated.sidebarAutoSettleAfterDays).toBe(7);
expect(updated.sidebarAutoSettleAfterHours).toBe(7);
expect(updated.sidebarAutoSettleOnMerge).toBe(true);
expect(updated.newWorktreesStartFromOrigin).toBe(true);
expect(updated.continueThreadsAfterServerUpdate).toBe(true);
Expand All @@ -70,7 +133,12 @@ describe("auto-settle settings sync", () => {
it("does not compare the reference or a target without loaded settings", () => {
const plan = planAutoSettleSettingsSync(reference, [
{ ...reference, label: "Reference" },
{ environmentId: EnvironmentId.make("loading"), label: "Loading", settings: null },
{
environmentId: EnvironmentId.make("loading"),
label: "Loading",
settings: null,
capabilities: reference.capabilities,
},
]);

expect(plan.mismatches).toEqual([]);
Expand Down
49 changes: 43 additions & 6 deletions apps/mobile/src/features/settings/autoSettleSettingsSync.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
import type { EnvironmentId, ServerSettings } from "@t3tools/contracts";
import { filterAutoSettleSettingsPatchForCapabilities } from "@t3tools/client-runtime/state/shared-settings";
import type {
EnvironmentId,
ExecutionEnvironmentCapabilities,
ServerSettings,
} from "@t3tools/contracts";

export type AutoSettleSettings = Pick<
ServerSettings,
"sidebarAutoSettleAfterDays" | "sidebarAutoSettleOnMerge"
"sidebarAutoSettleAfterHours" | "sidebarAutoSettleAfterDays" | "sidebarAutoSettleOnMerge"
>;

interface AutoSettleSyncTarget {
readonly environmentId: EnvironmentId;
readonly label: string;
readonly settings: AutoSettleSettings | null;
readonly capabilities?: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlementHours">;
}

interface AutoSettleWriteTarget {
readonly environmentId: EnvironmentId;
readonly capabilities?: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlementHours">;
}

export function planAutoSettleSettingsWrites(
patch: Partial<AutoSettleSettings>,
targets: readonly AutoSettleWriteTarget[],
) {
return targets.flatMap((target) => {
const filtered = filterAutoSettleSettingsPatchForCapabilities(patch, target.capabilities);
return Object.keys(filtered).length === 0
? []
: [{ environmentId: target.environmentId, patch: filtered }];
});
}

/** Receives connected, capable targets. Applying these defaults must preserve other settings. */
export function planAutoSettleSettingsSync(
reference: { readonly environmentId: EnvironmentId; readonly settings: AutoSettleSettings },
reference: {
readonly environmentId: EnvironmentId;
readonly settings: AutoSettleSettings;
readonly capabilities?: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlementHours">;
},
targets: readonly AutoSettleSyncTarget[],
) {
const patch: AutoSettleSettings = {
sidebarAutoSettleAfterDays: reference.settings.sidebarAutoSettleAfterDays,
const supportsHours = reference.capabilities?.threadAutoSettlementHours === true;
const patch: Partial<AutoSettleSettings> = {
...(supportsHours
? { sidebarAutoSettleAfterHours: reference.settings.sidebarAutoSettleAfterHours }
: reference.settings.sidebarAutoSettleAfterDays === undefined
? {}
: { sidebarAutoSettleAfterDays: reference.settings.sidebarAutoSettleAfterDays }),
sidebarAutoSettleOnMerge: reference.settings.sidebarAutoSettleOnMerge,
};
const mismatches = targets.filter(
(target) =>
target.environmentId !== reference.environmentId &&
target.settings !== null &&
(target.settings.sidebarAutoSettleAfterDays !== patch.sidebarAutoSettleAfterDays ||
(((target.capabilities?.threadAutoSettlementHours === true) === supportsHours &&
(supportsHours
? target.settings.sidebarAutoSettleAfterHours !==
reference.settings.sidebarAutoSettleAfterHours
: target.settings.sidebarAutoSettleAfterDays !==
reference.settings.sidebarAutoSettleAfterDays)) ||
target.settings.sidebarAutoSettleOnMerge !== patch.sidebarAutoSettleOnMerge),
);
return { patch, mismatches };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function AutoSettleDaysField(props: AutoSettleDaysFieldProps) {

return (
<View className="shrink-0 flex-row items-center">
{/* Match the 32dp switch track while retaining 48dp button touch targets. */}
<View
pointerEvents="none"
className="absolute inset-x-0 rounded-full bg-subtle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export function AutoSettleDaysField(props: AutoSettleDaysFieldProps) {
const commit = () => {
const text = (draft ?? "").trim();
setDraft(null);
// Validate the whole input; decimals and trailing text must not become whole days.
const parsed = /^\d+$/.test(text) ? Number(text) : Number.NaN;
if (
Number.isInteger(parsed) &&
Expand Down
Loading