diff --git a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
index b8feea241641..12ae305669ad 100644
--- a/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
+++ b/apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx
@@ -293,6 +293,13 @@ function CronJobSidebarRow({
})}
{renderActionItem(kit, { icon: 'watch', key: 'manage', label: c.manage, onSelect: onManage })}
+ {renderActionItem(kit, {
+ icon: 'settings',
+ key: 'change-model',
+ label: c.changeModel,
+ onSelect: onManage
+ })}
+
{renderActionItem(kit, {
icon: 'trash',
key: 'delete',
diff --git a/apps/desktop/src/app/cron/cron-job-model.ts b/apps/desktop/src/app/cron/cron-job-model.ts
index 9b800aea7053..46b8c6053553 100644
--- a/apps/desktop/src/app/cron/cron-job-model.ts
+++ b/apps/desktop/src/app/cron/cron-job-model.ts
@@ -45,6 +45,14 @@ export interface CronEditorSaveValues {
schedule: string
}
+/** Build the narrow update payload used by the detail-panel model picker. */
+export function cronModelUpdates(provider: string, model: string): CronJobUpdates {
+ return {
+ model: model.trim() || null,
+ provider: provider.trim() || null
+ }
+}
+
export function parseCronDeliveryTargets(value: string): string[] {
const targets = value
.split(',')
diff --git a/apps/desktop/src/app/cron/cron-model-switcher.test.ts b/apps/desktop/src/app/cron/cron-model-switcher.test.ts
new file mode 100644
index 000000000000..e02ede661cbe
--- /dev/null
+++ b/apps/desktop/src/app/cron/cron-model-switcher.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, it } from 'vitest'
+
+import { cronEditorUpdates, cronModelUpdates } from './cron-job-model'
+
+describe('cron model switcher — updateCronJob payload', () => {
+ it('pins a model and provider when both are provided', () => {
+ const updates = cronModelUpdates('anthropic', 'claude-sonnet-4')
+
+ expect(updates.model).toBe('claude-sonnet-4')
+ expect(updates.provider).toBe('anthropic')
+ })
+
+ it('clears a previous pin when model is empty', () => {
+ const updates = cronModelUpdates('', '')
+
+ expect(updates.model).toBeNull()
+ expect(updates.provider).toBeNull()
+ })
+
+ it('normalizes an empty provider when only a model is selected', () => {
+ const updates = cronModelUpdates(' ', ' gpt-4o ')
+
+ expect(updates.model).toBe('gpt-4o')
+ expect(updates.provider).toBeNull()
+ })
+})
+
+describe('cronEditorUpdates — model switcher integration', () => {
+ it('writes the model override for agent jobs (same path as the switcher)', () => {
+ const updates = cronEditorUpdates(
+ {
+ deliver: 'local',
+ model: 'deepseek-chat',
+ name: 'Daily summary',
+ prompt: 'go',
+ provider: 'deepseek',
+ schedule: '0 9 * * *'
+ },
+ { scriptOnlyJob: false }
+ )
+
+ expect(updates.model).toBe('deepseek-chat')
+ expect(updates.provider).toBe('deepseek')
+ })
+
+ it('clears a previous pin when the switcher resets to default', () => {
+ const updates = cronEditorUpdates(
+ { deliver: 'local', model: '', name: 'Daily', prompt: 'go', provider: '', schedule: '0 9 * * *' },
+ { scriptOnlyJob: false }
+ )
+
+ expect(updates.model).toBe(null)
+ expect(updates.provider).toBe(null)
+ })
+
+ it('uses a cheaper model for low-effort jobs and a powerful model for heavy jobs', () => {
+ // Low-effort summarising job
+ const cheapUpdates = cronEditorUpdates(
+ {
+ deliver: 'local',
+ model: 'deepseek-chat',
+ name: 'Feed summariser',
+ prompt: 'Summarise the feed',
+ provider: 'deepseek',
+ schedule: '0 * * * *'
+ },
+ { scriptOnlyJob: false }
+ )
+
+ expect(cheapUpdates.model).toBe('deepseek-chat')
+
+ // Heavy reasoning job
+ const heavyUpdates = cronEditorUpdates(
+ {
+ deliver: 'local',
+ model: 'claude-sonnet-4',
+ name: 'Weekly analysis',
+ prompt: 'Analyse the week',
+ provider: 'anthropic',
+ schedule: '0 9 * * 1'
+ },
+ { scriptOnlyJob: false }
+ )
+
+ expect(heavyUpdates.model).toBe('claude-sonnet-4')
+ })
+})
diff --git a/apps/desktop/src/app/cron/index.tsx b/apps/desktop/src/app/cron/index.tsx
index ee7f0af5aa2f..63470f901436 100644
--- a/apps/desktop/src/app/cron/index.tsx
+++ b/apps/desktop/src/app/cron/index.tsx
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query'
import type * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { ModelPickerDialog } from '@/components/model-picker'
import { PageLoader } from '@/components/page-loader'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
@@ -77,6 +78,7 @@ import { BlueprintSlotControl, blueprintSlotHelp, cleanBlueprintFieldError, init
import { mutateAndRefreshCronJobs, refreshCronJobs, triggerAndRefreshCronJobs } from './cron-actions'
import {
cronEditorUpdates,
+ cronModelUpdates,
jobIsScriptOnly,
parseCronDeliveryTargets,
toggleCronDeliveryTarget,
@@ -701,6 +703,19 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
busy={busyJobTokens.has(selectedJob.id) || triggeringJobKeys.has(`${profile}:${selectedJob.id}`)}
c={c}
job={selectedJob}
+ onModelChange={async (provider, model) => {
+ const { refreshError, stale } = await mutateAndRefreshCronJobs(profile, () =>
+ updateCronJob(selectedJob.id, cronModelUpdates(provider, model))
+ )
+
+ if (stale) {
+ return
+ }
+
+ if (refreshError) {
+ notifyError(refreshError, c.failedLoad)
+ }
+ }}
onOpenSession={onOpenSession}
onPauseResume={() => void handlePauseResume(selectedJob)}
onTrigger={() => void handleTrigger(selectedJob)}
@@ -781,6 +796,7 @@ function CronJobDetail({
busy,
c,
job,
+ onModelChange,
onOpenSession,
onPauseResume,
onTrigger
@@ -788,6 +804,7 @@ function CronJobDetail({
busy: boolean
c: Translations['cron']
job: CronJob
+ onModelChange?: (provider: string, model: string) => Promise
onOpenSession?: (sessionId: string) => void
onPauseResume: () => void
onTrigger: () => void
@@ -797,6 +814,9 @@ function CronJobDetail({
const deliver = jobDeliver(job)
const prompt = jobPrompt(job)
const modelOverride = jobModel(job)
+ const providerOverride = jobProvider(job)
+ const [modelPickerOpen, setModelPickerOpen] = useState(false)
+ const [modelBusy, setModelBusy] = useState(false)
return (
@@ -813,6 +833,11 @@ function CronJobDetail({
{c.triggerNow}
+ {onModelChange && !jobIsScriptOnly(job) && (
+ setModelPickerOpen(true)}>
+ {modelOverride || c.modelSwitcher}
+
+ )}
@@ -842,6 +867,28 @@ function CronJobDetail({
) : null}
+
+ {onModelChange && !jobIsScriptOnly(job) && (
+ {
+ setModelBusy(true)
+
+ try {
+ await onModelChange(provider, model)
+ notify({ kind: 'success', title: c.modelUpdated, message: truncate(jobTitle(job), 60) })
+ } catch {
+ notifyError(new Error(c.failedUpdate), c.failedUpdate)
+ } finally {
+ setModelBusy(false)
+ }
+ }}
+ open={modelPickerOpen}
+ />
+ )}
)
}
diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts
index 474effdc28ff..8d4b4f1c5b8e 100644
--- a/apps/desktop/src/i18n/ar.ts
+++ b/apps/desktop/src/i18n/ar.ts
@@ -1604,7 +1604,10 @@ export const ar = defineLocale({
optional: 'اختياري',
promptScheduleRequired: 'الرسالة والجدول مطلوبان',
saveChanges: 'حفظ التغييرات',
- createAction: 'إنشاء'
+ createAction: 'إنشاء',
+ modelSwitcher: 'النموذج',
+ modelUpdated: 'تم تحديث النموذج',
+ changeModel: 'تغيير النموذج'
},
artifacts: {
search: 'بحث',
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts
index aa1114eae62c..597972e848a0 100644
--- a/apps/desktop/src/i18n/en.ts
+++ b/apps/desktop/src/i18n/en.ts
@@ -2045,6 +2045,9 @@ export const en: Translations = {
scriptOnlyEditHint: 'Script-only job (no AI prompt). Job id:',
saveChanges: 'Save changes',
createAction: 'Create cron',
+ modelSwitcher: 'Model',
+ modelUpdated: 'Model updated',
+ changeModel: 'Change model',
tabs: {
jobs: 'Jobs',
blueprints: 'Blueprints'
diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts
index 721f2b0c03ff..96a1b8840fb4 100644
--- a/apps/desktop/src/i18n/ja.ts
+++ b/apps/desktop/src/i18n/ja.ts
@@ -1757,6 +1757,9 @@ export const ja = defineLocale({
scriptOnlyEditHint: 'スクリプトのみのジョブ(AI プロンプトなし)。ジョブ ID:',
saveChanges: '変更を保存',
createAction: 'Cron を作成',
+ modelSwitcher: 'モデル',
+ modelUpdated: 'モデルを更新しました',
+ changeModel: 'モデルを変更',
tabs: {
jobs: 'ジョブ',
blueprints: 'ブレーンプリント'
diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts
index 777e6df7a4b2..ce2a5abcbd90 100644
--- a/apps/desktop/src/i18n/types.ts
+++ b/apps/desktop/src/i18n/types.ts
@@ -1718,6 +1718,9 @@ export interface Translations {
scriptOnlyEditHint: string
saveChanges: string
createAction: string
+ modelSwitcher: string
+ modelUpdated: string
+ changeModel: string
tabs: {
jobs: string
blueprints: string
diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts
index 24aac3e97807..fdd06d2b5da5 100644
--- a/apps/desktop/src/i18n/zh-hant.ts
+++ b/apps/desktop/src/i18n/zh-hant.ts
@@ -1694,6 +1694,9 @@ export const zhHant = defineLocale({
scriptOnlyEditHint: '僅腳本任務(無 AI 提示詞)。任務 ID:',
saveChanges: '儲存變更',
createAction: '建立排程工作',
+ modelSwitcher: '模型',
+ modelUpdated: '模型已更新',
+ changeModel: '變更模型',
tabs: {
jobs: '工作',
blueprints: '藍圖'
diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts
index edb2d9c2d553..bd6b318c82d0 100644
--- a/apps/desktop/src/i18n/zh.ts
+++ b/apps/desktop/src/i18n/zh.ts
@@ -2227,6 +2227,9 @@ export const zh: Translations = {
scriptOnlyEditHint: '仅脚本任务(无 AI 提示词)。任务 ID:',
saveChanges: '保存更改',
createAction: '创建定时任务',
+ modelSwitcher: '模型',
+ modelUpdated: '模型已更新',
+ changeModel: '更改模型',
tabs: {
jobs: '任务',
blueprints: '蓝图'