diff --git a/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx
index 8eeaf2e567ed1..d71fbc8fd9d24 100644
--- a/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx
+++ b/apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx
@@ -83,7 +83,11 @@ import {
type ScheduledTaskDraft as Draft,
} from "./scheduledTaskDraft";
import { settingsTargetsForProject } from "./settings-environment-filter.logic";
-import { useScheduledTaskDestinationLabel, useScheduledTaskEditor } from "./scheduled-task-editor";
+import {
+ useScheduledTaskDestinationLabel,
+ useScheduledTaskEditor,
+ useScheduledTaskThread,
+} from "./scheduled-task-editor";
import {
discardScheduledTaskEditor,
markScheduledTaskEditorSaved,
@@ -693,6 +697,10 @@ function TaskForm({
.sort((left, right) => Number(right.pinnedAt != null) - Number(left.pinnedAt != null));
const config = useEnvironmentServerConfig(environmentId);
const destinationLabel = useScheduledTaskDestinationLabel(environmentId, draft.threadId);
+ const boundThread = useScheduledTaskThread(environmentId, draft.threadId);
+ // Bound runs use the thread's current model, so the locked row shows that one.
+ const shownSelection =
+ draft.threadId === null ? draft.modelSelection : (boundThread?.modelSelection ?? null);
const modelOptions = buildModelOptions(config, null, undefined, draft.projectId);
// Existing webhook tasks keep the option so they can still switch to a timer.
const webhookDenial = managedCapabilityDenial(config?.managedPolicy, "webhookTasks");
@@ -929,14 +937,17 @@ function TaskForm({
value={
modelOptions.find(
(option) =>
- option.selection.instanceId === draft.modelSelection?.instanceId &&
- option.selection.model === draft.modelSelection?.model,
+ option.selection.instanceId === shownSelection?.instanceId &&
+ option.selection.model === shownSelection?.model,
)?.label ??
- draft.modelSelection?.model ??
+ shownSelection?.model ??
(modelOptions.length ? "Choose model" : "No models available")
}
onPress={() => navigation.navigate("SettingsScheduledTaskModel")}
- disabled={saving || dictationPending || environmentUnavailable}
+ // The saved selection only seeds new threads; bound runs ignore it.
+ disabled={
+ saving || dictationPending || environmentUnavailable || draft.threadId !== null
+ }
/>
diff --git a/apps/mobile/src/features/settings/scheduled-task-editor.tsx b/apps/mobile/src/features/settings/scheduled-task-editor.tsx
index e99600ae533b8..d79ef40896e87 100644
--- a/apps/mobile/src/features/settings/scheduled-task-editor.tsx
+++ b/apps/mobile/src/features/settings/scheduled-task-editor.tsx
@@ -21,22 +21,29 @@ import {
export const NEW_THREAD_DESTINATION_LABEL = "New thread each run";
-export function useScheduledTaskDestinationLabel(
+export function useScheduledTaskThread(
environmentId: EnvironmentId | null,
threadId: ThreadId | null,
-): string {
+) {
const ref =
environmentId === null || threadId === null ? null : scopeThreadRef(environmentId, threadId);
const shell = useThreadShell(ref);
- // The shell can omit older settled threads; their family read still carries the title.
+ // The shell can omit older settled threads; their family read still carries the row.
const family = useEnvironmentQuery(
ref !== null && shell === null
? threadFamilies.familyAtom(ref.environmentId, ref.threadId)
: null,
);
- const thread = shell ?? family.data?.threads.find((row) => row.id === threadId);
- if (ref === null) return NEW_THREAD_DESTINATION_LABEL;
- return thread?.title.trim() || `Thread ${ref.threadId}`;
+ return shell ?? family.data?.threads.find((row) => row.id === threadId) ?? null;
+}
+
+export function useScheduledTaskDestinationLabel(
+ environmentId: EnvironmentId | null,
+ threadId: ThreadId | null,
+): string {
+ const thread = useScheduledTaskThread(environmentId, threadId);
+ if (environmentId === null || threadId === null) return NEW_THREAD_DESTINATION_LABEL;
+ return thread?.title.trim() || `Thread ${threadId}`;
}
/** The form draft lives in an atom, so it survives while its native picker routes are on top. */
diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx
index e2976652ea872..de655f374af5e 100644
--- a/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx
+++ b/apps/web/src/components/settings/ScheduledTasksSettings.browser.test.tsx
@@ -45,7 +45,13 @@ const task: ScheduledTask = {
// Stands in for the thread family read the editor makes when the shell lacks the bound thread.
const threadFamilyRead = {
snapshotSequence: 1,
- threads: [{ id: task.threadId, title: "Fix login bug" }],
+ threads: [
+ {
+ id: task.threadId,
+ title: "Fix login bug",
+ modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.5" },
+ },
+ ],
};
const state = vi.hoisted(() => ({
@@ -244,6 +250,19 @@ describe("ScheduledTasksSettings", () => {
await expect.element(name).toHaveValue("Review stale issues");
});
+ it("offers no model choice for a task bound to a thread, whose runs use the thread's model", async () => {
+ setup();
+ await using _screen = await render();
+ await expect.element(page.getByRole("dialog").getByText("Fix login bug")).toBeVisible();
+ const enabledPicker = document.querySelector(
+ '[role="dialog"] [data-chat-provider-model-picker]:not(:disabled)',
+ );
+ expect(enabledPicker).toBeNull();
+ await expect
+ .element(page.getByRole("dialog").getByRole("button", { name: "gpt-5.5", exact: true }))
+ .toBeDisabled();
+ });
+
it("keeps an editor the user opened when the linked task loads later", async () => {
setup();
state.tasksLoaded = false;
diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx
index b5b01ddd58b51..5e83dd1baadd9 100644
--- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx
+++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx
@@ -1188,6 +1188,16 @@ const THREAD_MODE_LABELS: Record = {
fresh_session: "One thread, fresh session each run",
};
+function useScheduledTaskThread(environmentId: EnvironmentId, threadId: ThreadId | null) {
+ const ref = threadId === null ? null : scopeThreadRef(environmentId, threadId);
+ const shell = useThreadShell(ref);
+ // The shell can omit older settled threads; their family read still carries the row.
+ const family = useEnvironmentQuery(
+ ref !== null && shell === null ? threadFamilies.familyAtom(environmentId, ref.threadId) : null,
+ );
+ return shell ?? family.data?.threads.find((row) => row.id === threadId) ?? null;
+}
+
function ScheduledTaskDestination({
environmentId,
projectId,
@@ -1209,12 +1219,7 @@ function ScheduledTaskDestination({
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const ref = threadId === null ? null : scopeThreadRef(environmentId, threadId);
- const shell = useThreadShell(ref);
- // The shell can omit older settled threads; their family read still carries the title.
- const family = useEnvironmentQuery(
- ref !== null && shell === null ? threadFamilies.familyAtom(environmentId, ref.threadId) : null,
- );
- const thread = shell ?? family.data?.threads.find((row) => row.id === threadId);
+ const thread = useScheduledTaskThread(environmentId, threadId);
const currentLabel =
ref !== null
? thread?.title.trim() || `Thread ${ref.threadId}`
@@ -1428,11 +1433,21 @@ function ScheduledTaskEditorForm({
const activeInstanceId =
activeSelection?.instanceId ?? firstInstance?.instanceId ?? ("" as ProviderInstanceId);
const activeModel = activeSelection?.model ?? "";
+ const boundThread = useScheduledTaskThread(
+ environmentId,
+ draft.threadId ? (draft.threadId as ThreadId) : null,
+ );
+ // Bound runs use the thread's current model, so the locked picker shows that one.
+ const shownSelection = draft.threadId
+ ? (boundThread?.modelSelection ?? null)
+ : { instanceId: activeInstanceId, model: activeModel };
+ const shownInstanceId = shownSelection?.instanceId ?? activeInstanceId;
+ const shownModel = shownSelection?.model ?? "";
const modelOptionsByInstance = getCustomModelOptionsByInstance(
settings,
providers,
- activeInstanceId,
- activeModel,
+ shownInstanceId,
+ shownModel,
);
const reportFailure = (title: string, error: unknown) => {
@@ -1777,9 +1792,10 @@ function ScheduledTaskEditorForm({