diff --git a/CHANGELOG.md b/CHANGELOG.md index 7725e0c0..9b361d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Added +- **Pick the exact time and day for scheduled jobs — no cron syntax needed.** The Tasks "New job" form now builds the schedule as a plain sentence: choose a frequency (Hourly / Daily / Weekdays / Weekly / Monthly / Custom) and it shows just the controls that matter — a time picker, a day-of-week dropdown, or a day-of-month dropdown — e.g. "Weekly on Monday at 09:00." The generated cron expression is previewed inline, and the raw cron field now appears only under "Custom." Thanks @rodboev. (#5554, #5552) - **Transparent Stream fades newly streamed words in.** In Transparent Stream mode, newly arriving assistant words now fade in at the prose level as they stream — without animating (or re-flickering) the thinking rows, tool rows, or the transparent event rows. It respects your OS "reduce motion" setting (no fade when reduced motion is on) and is part of the Transparent Stream experience regardless of the "Fade text effect" toggle. Thanks @rodboev. (#5506, #5367) - **The Preferences default-model setting now uses the rich model picker.** The "Default Model" control in Settings → Preferences was a plain dropdown; it now uses the same searchable, provider-grouped picker as the composer (with a "Custom Model ID" field), so choosing a default model is consistent everywhere. On touch devices the picker no longer pops the keyboard the instant you open it (it still focuses search on desktop and while you're typing), and its label now activates the picker directly. Thanks @rodboev. (#5502, #5497) - **Scheduled jobs get a preset schedule builder.** The cron job form now has a "Preset" dropdown (Hourly / Daily / Weekdays / Weekly / Monthly, plus Custom) above the Schedule field — pick one and it fills the schedule for you (e.g. Daily → `0 9 * * *`, Hourly → `every 1h`) instead of hand-writing a cron expression. "Custom" keeps the free-text field, and editing an existing job maps its schedule back to the matching preset. Thanks @rodboev. (#5438, #5427) diff --git a/static/i18n.js b/static/i18n.js index 3e4362ba..88832e47 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -1584,18 +1584,30 @@ const LOCALES = { // cron form cron_name_label: 'Name', cron_name_placeholder: 'Optional', - cron_schedule_label: 'Schedule', - cron_schedule_preset_label: 'Preset', + cron_schedule_label: 'Cron expression', + cron_schedule_preset_label: 'Schedule', cron_schedule_preset_hourly: 'Hourly', cron_schedule_preset_daily: 'Daily', - cron_schedule_preset_weekdays: 'Weekdays', + cron_schedule_preset_weekdays: 'Weekdays (Mon–Fri)', cron_schedule_preset_weekly: 'Weekly', cron_schedule_preset_monthly: 'Monthly', cron_schedule_preset_custom: 'Custom', cron_schedule_hour_label: 'Hour', cron_schedule_minute_label: 'Minute', - cron_schedule_weekday_label: 'Weekday (0-6, Sun-Sat)', - cron_schedule_month_day_label: 'Day', + cron_schedule_weekday_label: 'Day of week', + cron_schedule_month_day_label: 'Day of month', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_time_hint: 'Time is server time; cron runs server-side.', cron_schedule_hint: "Use 'every 1h' or a cron expression for recurring jobs. Bare durations like '30m' run once.", cron_schedule_once_warning: "Duration forms like '30m' run once and are removed after running. Use 'every 30m' to keep a recurring job.", @@ -3263,6 +3275,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Ogni settimana', cron_schedule_preset_monthly: 'Ogni mese', cron_schedule_preset_custom: 'Personalizzato', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Ora', cron_schedule_minute_label: 'Minuto', cron_schedule_weekday_label: 'Giorno della settimana (0-6, Dom-Sab)', @@ -4942,6 +4966,18 @@ const LOCALES = { cron_schedule_preset_weekly: '毎週', cron_schedule_preset_monthly: '毎月', cron_schedule_preset_custom: 'カスタム', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: '時', cron_schedule_minute_label: '分', cron_schedule_weekday_label: '曜日 (0-6, 日-土)', @@ -6263,6 +6299,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Еженедельно', cron_schedule_preset_monthly: 'Ежемесячно', cron_schedule_preset_custom: 'Пользовательский', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Час', cron_schedule_minute_label: 'Минута', cron_schedule_weekday_label: 'День недели (0-6, вс-сб)', @@ -7861,6 +7909,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Semanal', cron_schedule_preset_monthly: 'Mensual', cron_schedule_preset_custom: 'Personalizado', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Hora', cron_schedule_minute_label: 'Minuto', cron_schedule_weekday_label: 'Día de la semana (0-6, dom-sáb)', @@ -9141,6 +9201,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Wöchentlich', cron_schedule_preset_monthly: 'Monatlich', cron_schedule_preset_custom: 'Benutzerdefiniert', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Stunde', cron_schedule_minute_label: 'Minute', cron_schedule_weekday_label: 'Wochentag (0-6, So-Sa)', @@ -11093,6 +11165,18 @@ const LOCALES = { cron_schedule_preset_weekly: '每周', cron_schedule_preset_monthly: '每月', cron_schedule_preset_custom: '自定义', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: '小时', cron_schedule_minute_label: '分钟', cron_schedule_weekday_label: '星期 (0-6,周日-周六)', @@ -13066,6 +13150,18 @@ const LOCALES = { cron_schedule_preset_weekly: '每週', cron_schedule_preset_monthly: '每月', cron_schedule_preset_custom: '自訂', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: '小時', cron_schedule_minute_label: '分鐘', cron_schedule_weekday_label: '星期 (0-6,週日-週六)', @@ -14478,6 +14574,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Semanal', cron_schedule_preset_monthly: 'Mensal', cron_schedule_preset_custom: 'Personalizado', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Hora', cron_schedule_minute_label: 'Minuto', cron_schedule_weekday_label: 'Dia da semana (0-6, dom-sáb)', @@ -16133,6 +16241,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Weekly', cron_schedule_preset_monthly: 'Monthly', cron_schedule_preset_custom: 'Custom', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: '시간', cron_schedule_minute_label: '분', cron_schedule_weekday_label: '요일 (0-6, 일-토)', @@ -17880,6 +18000,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Hebdomadaire', cron_schedule_preset_monthly: 'Mensuel', cron_schedule_preset_custom: 'Personnalisé', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Heure', cron_schedule_minute_label: 'Minute', cron_schedule_weekday_label: 'Jour de la semaine (0-6, dim-sam)', @@ -19459,6 +19591,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Haftalık', cron_schedule_preset_monthly: 'Aylık', cron_schedule_preset_custom: 'Özel', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Saat', cron_schedule_minute_label: 'Dakika', cron_schedule_weekday_label: 'Haftanın günü (0-6, Paz-Cmt)', @@ -21218,6 +21362,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Co tydzień', cron_schedule_preset_monthly: 'Co miesiąc', cron_schedule_preset_custom: 'Niestandardowe', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Godzina', cron_schedule_minute_label: 'Minuta', cron_schedule_weekday_label: 'Dzień tygodnia (0-6, niedz.-sob.)', @@ -22731,6 +22887,18 @@ const LOCALES = { cron_schedule_preset_weekly: 'Hàng tuần', cron_schedule_preset_monthly: 'Hàng tháng', cron_schedule_preset_custom: 'Tùy chỉnh', + cron_schedule_time_label: 'Time', + cron_schedule_conj_on: 'on', + cron_schedule_conj_on_day: 'on day', + cron_schedule_conj_at: 'at', + cron_schedule_conj_at_minute: 'at minute', + cron_weekday_sun: 'Sunday', + cron_weekday_mon: 'Monday', + cron_weekday_tue: 'Tuesday', + cron_weekday_wed: 'Wednesday', + cron_weekday_thu: 'Thursday', + cron_weekday_fri: 'Friday', + cron_weekday_sat: 'Saturday', cron_schedule_hour_label: 'Giờ', cron_schedule_minute_label: 'Phút', cron_schedule_weekday_label: 'Thứ trong tuần (0-6, CN-T7)', diff --git a/static/panels.js b/static/panels.js index 8a481301..13ba281e 100644 --- a/static/panels.js +++ b/static/panels.js @@ -464,15 +464,29 @@ function _syncCronScheduleWarning() { const warning = $('cronFormScheduleOnceWarning'); if (!input || !warning) return; warning.style.display = _cronScheduleKindForInput(input.value) === 'once' ? '' : 'none'; + _syncCronSchedulePreview(); +} + +// Live preview of the generated cron expression in the preset hint line (the +// cron-job.org / GitHub-schedule-editor convention) so a cron-literate user sees +// exactly what the friendly controls produce. Empty on Custom (the raw field is shown). +function _syncCronSchedulePreview() { + const preview = $('cronFormSchedulePreview'); + const presetEl = $('cronFormSchedulePreset'); + const scheduleEl = $('cronFormSchedule'); + if (!preview) return; + const presetId = presetEl ? presetEl.value : 'custom'; + const expr = String((scheduleEl && scheduleEl.value) || '').trim(); + preview.textContent = (presetId !== 'custom' && expr) ? `${expr} · ` : ''; } const CRON_SCHEDULE_PRESETS = [ - { id: 'custom', label: 'cron_schedule_preset_custom', fallback: 'Custom', fields: [] }, { id: 'hourly', label: 'cron_schedule_preset_hourly', fallback: 'Hourly', fields: ['minute'], defaults: { minute: 0 } }, - { id: 'daily', label: 'cron_schedule_preset_daily', fallback: 'Daily', fields: ['hour', 'minute'], defaults: { hour: 9, minute: 0 } }, - { id: 'weekdays', label: 'cron_schedule_preset_weekdays', fallback: 'Weekdays', fields: ['hour', 'minute'], defaults: { hour: 9, minute: 0 } }, - { id: 'weekly', label: 'cron_schedule_preset_weekly', fallback: 'Weekly', fields: ['hour', 'minute', 'weekday'], defaults: { hour: 9, minute: 0, weekday: 1 } }, - { id: 'monthly', label: 'cron_schedule_preset_monthly', fallback: 'Monthly', fields: ['hour', 'minute', 'monthDay'], defaults: { hour: 9, minute: 0, monthDay: 1 } }, + { id: 'daily', label: 'cron_schedule_preset_daily', fallback: 'Daily', fields: ['time'], defaults: { hour: 9, minute: 0 } }, + { id: 'weekdays', label: 'cron_schedule_preset_weekdays', fallback: 'Weekdays (Mon–Fri)', fields: ['time'], defaults: { hour: 9, minute: 0 } }, + { id: 'weekly', label: 'cron_schedule_preset_weekly', fallback: 'Weekly', fields: ['weekday', 'time'], defaults: { hour: 9, minute: 0, weekday: 1 } }, + { id: 'monthly', label: 'cron_schedule_preset_monthly', fallback: 'Monthly', fields: ['monthDay', 'time'], defaults: { hour: 9, minute: 0, monthDay: 1 } }, + { id: 'custom', label: 'cron_schedule_preset_custom', fallback: 'Custom', fields: [] }, ]; function _cronSchedulePresetOptionHtml() { @@ -487,13 +501,24 @@ function _cronSchedulePresetForId(presetId) { function _cronSchedulePresetControlIds() { return { - hour: 'cronFormScheduleHour', + time: 'cronFormScheduleTime', minute: 'cronFormScheduleMinute', weekday: 'cronFormScheduleWeekday', monthDay: 'cronFormScheduleMonthDay', }; } +// Which visible control wrapper each logical field lives in. `hour`+`minute` for +// time-based presets share the single #cronFormScheduleTime picker (in the Time +// field); `minute` alone (Hourly) uses the standalone Minute field. +function _cronSchedulePresetFieldWrapId(field) { + if (field === 'time' || field === 'hour') return 'cronFormScheduleTimeField'; + if (field === 'minute') return 'cronFormScheduleMinuteField'; + if (field === 'weekday') return 'cronFormScheduleWeekdayField'; + if (field === 'monthDay') return 'cronFormScheduleMonthDayField'; + return ''; +} + function _cronSchedulePresetFieldId(field) { const ids = _cronSchedulePresetControlIds(); return ids[field] || ''; @@ -522,11 +547,34 @@ function _cronSchedulePresetNormalizeValue(field, value, fallback) { } function _cronSchedulePresetValueForField(field, fallback) { + // hour/minute for time-based presets come from the single #cronFormScheduleTime + // picker ("HH:MM"); the standalone Minute box (Hourly) still reads directly. + if (field === 'hour' || field === 'minute') { + const timeEl = $('cronFormScheduleTime'); + const minuteBox = $('cronFormScheduleMinute'); + // Hourly uses the standalone minute box; if the Time picker isn't the visible + // source for minute, prefer the minute box when it's the shown control. + if (field === 'minute' && minuteBox && (!timeEl || _cronScheduleMinuteBoxIsActive())) { + return _cronSchedulePresetNormalizeValue('minute', minuteBox.value, fallback); + } + if (timeEl && /^\d{1,2}:\d{2}$/.test(String(timeEl.value || '').trim())) { + const [h, m] = String(timeEl.value).trim().split(':'); + return _cronSchedulePresetNormalizeValue(field, field === 'hour' ? h : m, fallback); + } + return _cronSchedulePresetNormalizeValue(field, fallback, fallback); + } const el = _cronSchedulePresetFieldEl(field); const raw = el ? String(el.value || '').trim() : ''; return _cronSchedulePresetNormalizeValue(field, raw, fallback); } +// The standalone Minute box is the active minute source only for the Hourly preset +// (the only preset whose visible fields include a bare 'minute'). +function _cronScheduleMinuteBoxIsActive() { + const presetEl = $('cronFormSchedulePreset'); + return !!(presetEl && presetEl.value === 'hourly'); +} + function _cronSchedulePresetRawFieldInBounds(field, value) { const raw = String(value || '').trim(); if (!/^\d+$/.test(raw)) return false; @@ -536,7 +584,20 @@ function _cronSchedulePresetRawFieldInBounds(field, value) { } function _cronSchedulePresetApplyValues(values) { - ['hour', 'minute', 'weekday', 'monthDay'].forEach((field) => { + // Write hour/minute into the single time picker as zero-padded HH:MM. + if (values.hour != null || values.minute != null) { + const timeEl = $('cronFormScheduleTime'); + if (timeEl) { + const h = _cronSchedulePresetNormalizeValue('hour', values.hour, values.hour ?? 9); + const m = _cronSchedulePresetNormalizeValue('minute', values.minute, values.minute ?? 0); + timeEl.value = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; + } + } + if (values.minute != null) { + const minuteBox = $('cronFormScheduleMinute'); + if (minuteBox) minuteBox.value = _cronSchedulePresetNormalizeValue('minute', values.minute, values.minute); + } + ['weekday', 'monthDay'].forEach((field) => { const el = _cronSchedulePresetFieldEl(field); if (!el || values[field] == null) return; el.value = _cronSchedulePresetNormalizeValue(field, values[field], values[field]); @@ -545,11 +606,16 @@ function _cronSchedulePresetApplyValues(values) { function _cronSchedulePresetSyncVisibility(presetId) { const wrapper = $('cronFormSchedulePresetParams'); + const customRow = $('cronFormScheduleCustomRow'); const preset = _cronSchedulePresetForId(presetId); const showFields = preset && Array.isArray(preset.fields) ? preset.fields : []; - if (wrapper) wrapper.style.display = presetId === 'custom' ? 'none' : ''; - ['hour', 'minute', 'weekday', 'monthDay'].forEach((field) => { - const fieldWrap = $(`cronFormSchedule${field === 'monthDay' ? 'MonthDay' : field.charAt(0).toUpperCase() + field.slice(1)}Field`); + const isCustom = presetId === 'custom'; + // Preset param controls hide entirely on Custom; the raw cron expression row + // shows ONLY on Custom (kept in the DOM as a hidden field otherwise). + if (wrapper) wrapper.style.display = isCustom ? 'none' : ''; + if (customRow) customRow.style.display = isCustom ? '' : 'none'; + ['time', 'minute', 'weekday', 'monthDay'].forEach((field) => { + const fieldWrap = $(_cronSchedulePresetFieldWrapId(field)); if (fieldWrap) fieldWrap.style.display = showFields.includes(field) ? '' : 'none'; }); } @@ -658,20 +724,44 @@ function _applyCronSchedulePresetSelection() { _syncCronScheduleWarning(); } +// Regenerate the cron expression from the current field values WITHOUT writing the +// clamped values back into the field the user is editing — so typing into the Minute +// box (or the time picker) doesn't snap a half-entered value to the default/clamp +// mid-keystroke. Value clamping still happens on `change`/blur via the full apply. +function _regenCronScheduleFromFields() { + const presetEl = $('cronFormSchedulePreset'); + const scheduleEl = $('cronFormSchedule'); + if (!presetEl || !scheduleEl) return; + const presetId = presetEl.value; + if (presetId === 'custom') return; + scheduleEl.value = _cronSchedulePresetValueForSelection(presetId); + _syncCronScheduleWarning(); +} + function _initCronSchedulePresetControls() { const presetEl = $('cronFormSchedulePreset'); const scheduleEl = $('cronFormSchedule'); if (!presetEl || !scheduleEl) return; presetEl.addEventListener('change', _applyCronSchedulePresetSelection); if ($('cronFormSchedulePresetParams')) { - ['cronFormScheduleHour', 'cronFormScheduleMinute', 'cronFormScheduleWeekday', 'cronFormScheduleMonthDay'].forEach((id) => { + ['cronFormScheduleTime', 'cronFormScheduleMinute', 'cronFormScheduleWeekday', 'cronFormScheduleMonthDay'].forEach((id) => { const el = $(id); if (!el) return; + // On `change`/blur, clamp + normalize (writes values back). On `input` + // (per-keystroke), only regenerate the expression — never rewrite the field + // being typed into, so partial input like clearing Hour to type "14" isn't + // snapped to the default (#5554 UX fix). el.addEventListener('change', _applyCronSchedulePresetSelection); - el.addEventListener('input', _applyCronSchedulePresetSelection); + el.addEventListener('input', _regenCronScheduleFromFields); }); } - scheduleEl.addEventListener('input', _syncCronSchedulePresetAndWarning); + // On raw-cron `input` (only reachable on Custom, where the raw row is shown), + // update ONLY the warning + preview — do NOT re-detect the preset or sync + // visibility, or a partial value momentarily matching a preset (e.g. typing + // "0 9 * * 1,3" transiently equals Weekly's "0 9 * * 1") would switch the preset + // and hide the focused raw field mid-keystroke (#5554). Preset re-detection runs + // on initial render and on `change`/blur. + scheduleEl.addEventListener('input', _syncCronScheduleWarning); scheduleEl.addEventListener('change', _syncCronSchedulePresetAndWarning); _syncCronSchedulePresetAndWarning(); } @@ -1451,7 +1541,7 @@ function openCronCreate(){ _cronMode = 'create'; _cronIsDuplicate = false; _cronSelectedSkills = []; - _renderCronForm({ name:'', schedule:'', prompt:'', deliver:'local', profile:'', toast_notifications:true, model:'', provider:'', isEdit:false }); + _renderCronForm({ name:'', schedule:'0 9 * * *', prompt:'', deliver:'local', profile:'', toast_notifications:true, model:'', provider:'', isEdit:false }); _cronSkillsCache = null; api('/api/skills').then(d=>{_cronSkillsCache=d.skills||[]; _bindCronSkillPicker();}).catch(()=>{}); loadCronProfiles().then(()=>_refreshCronProfileSelect('')).catch(()=>{}); @@ -1522,34 +1612,44 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, toast_notif