diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
index 9fb5b05d04..a106503220 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
@@ -47,27 +47,25 @@ export interface PlusMenuHandle {
/**
* Box and typography shared by the textarea and its mirror overlay — both must
* produce identical line wrapping so the overlay text sits exactly over the
- * (transparent) textarea text. The scale is the canonical chip text-field
- * scale ({@link ChipTextarea}: `text-sm`, default tracking), so the editor
- * reads identically in the chat input and inside chip modals — one size,
- * everywhere.
+ * (transparent) textarea text. The scale is the chat input's native prompt
+ * scale (`text-[15px]`, `-0.015em` tracking); the task modal's body inherits it
+ * so the editor reads the same whether it's the chat input or inside the modal.
*/
const FIELD_MIRROR_CLASSES = cn(
- 'm-0 box-border min-h-[20px] w-full break-words [overflow-wrap:anywhere] border-0 bg-transparent',
- 'px-1 py-1 font-body text-sm leading-[20px]'
+ 'm-0 box-border min-h-[24px] w-full break-words [overflow-wrap:anywhere] border-0 bg-transparent',
+ 'px-1 py-1 font-body text-[15px] leading-[24px] tracking-[-0.015em]'
)
/**
* The textarea grows to its full content height (`h-auto`, no internal scroll);
* the shared scroller clips and scrolls it. Its text is transparent so the
- * mirror overlay shows through; only the caret paints. The placeholder uses
- * the canonical `--text-muted`, matching every other chip text field.
+ * mirror overlay shows through; only the caret paints.
*/
export const TEXTAREA_BASE_CLASSES = cn(
FIELD_MIRROR_CLASSES,
'block h-auto resize-none overflow-hidden',
'text-transparent caret-[var(--text-primary)] outline-none',
- 'placeholder:text-[var(--text-muted)]',
+ 'placeholder:font-[380] placeholder:text-[var(--text-subtle)]',
'focus-visible:ring-0 focus-visible:ring-offset-0'
)
diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control.tsx
deleted file mode 100644
index 54cc65d572..0000000000
--- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-'use client'
-
-import { format } from 'date-fns'
-import { ChipDatePicker, ChipDropdown, ChipInput, RefreshCw } from '@/components/emcn'
-import type { Recurrence } from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/recurrence'
-
-const WEEKDAY_PRESET = [1, 2, 3, 4, 5]
-/** Seed count when the user first chooses "ends after N runs". */
-const DEFAULT_END_AFTER_COUNT = 10
-
-/** The frequency presets the dropdown authors, keyed by a synthetic option value. */
-type FrequencyOption = 'once' | 'daily' | 'weekly' | 'weekdays' | 'monthly' | 'custom'
-
-function isWeekdayPreset(weekdays: number[]): boolean {
- return (
- weekdays.length === WEEKDAY_PRESET.length && WEEKDAY_PRESET.every((d) => weekdays.includes(d))
- )
-}
-
-/** Collapses a recurrence into the single dropdown value that represents it. */
-function frequencyOptionFor(recurrence: Recurrence): FrequencyOption {
- if (recurrence.frequency === 'weekly')
- return isWeekdayPreset(recurrence.weekdays) ? 'weekdays' : 'weekly'
- return recurrence.frequency
-}
-
-interface RecurrenceControlProps {
- recurrence: Recurrence
- onChange: (recurrence: Recurrence) => void
- /** The launch day, so weekly/monthly labels name the weekday and day-of-month. */
- launchDate: string
-}
-
-/**
- * The repeat + end controls for a scheduled task, modeled on a calendar app's
- * recurrence row: a frequency preset and — when the task repeats — how it ends
- * (never, on a date, or after N runs).
- */
-export function RecurrenceControl({ recurrence, onChange, launchDate }: RecurrenceControlProps) {
- const launch = new Date(`${launchDate}T00:00`)
-
- const frequencyOptions = [
- { value: 'once', label: 'Once' },
- { value: 'daily', label: 'Daily' },
- { value: 'weekly', label: `Weekly on ${format(launch, 'EEE')}` },
- { value: 'weekdays', label: 'Weekdays' },
- { value: 'monthly', label: `Monthly on the ${format(launch, 'do')}` },
- ...(recurrence.frequency === 'custom' ? [{ value: 'custom', label: 'Custom' }] : []),
- ]
-
- const handleFrequencyChange = (value: string) => {
- const option = value as FrequencyOption
- switch (option) {
- case 'once':
- onChange({ frequency: 'once', weekdays: [], end: { type: 'never' } })
- return
- case 'daily':
- onChange({ ...recurrence, frequency: 'daily', weekdays: [] })
- return
- case 'weekly':
- onChange({ ...recurrence, frequency: 'weekly', weekdays: [launch.getDay()] })
- return
- case 'weekdays':
- onChange({ ...recurrence, frequency: 'weekly', weekdays: [...WEEKDAY_PRESET] })
- return
- case 'monthly':
- onChange({ ...recurrence, frequency: 'monthly', weekdays: [] })
- return
- case 'custom':
- onChange({ ...recurrence, frequency: 'custom' })
- }
- }
-
- const handleEndChange = (value: string) => {
- if (value === 'never') onChange({ ...recurrence, end: { type: 'never' } })
- else if (value === 'on')
- onChange({ ...recurrence, end: { type: 'on', date: format(launch, 'yyyy-MM-dd') } })
- else {
- const count = recurrence.end.type === 'after' ? recurrence.end.count : DEFAULT_END_AFTER_COUNT
- onChange({ ...recurrence, end: { type: 'after', count } })
- }
- }
-
- return (
-
-
-
- {recurrence.frequency !== 'once' && (
-
- )}
-
- {recurrence.frequency !== 'once' && recurrence.end.type === 'on' && (
- onChange({ ...recurrence, end: { type: 'on', date } })}
- flush
- />
- )}
-
- {recurrence.frequency !== 'once' && recurrence.end.type === 'after' && (
- {
- const count = Math.max(1, Math.floor(Number(event.target.value) || 1))
- onChange({ ...recurrence, end: { type: 'after', count } })
- }}
- endAdornment={runs}
- />
- )}
-
- )
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx
new file mode 100644
index 0000000000..d74ca387fb
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section.tsx
@@ -0,0 +1,185 @@
+'use client'
+
+import { useRef } from 'react'
+import { format } from 'date-fns'
+import { ChipDatePicker, ChipModalField, ChipModalSeparator, Switch } from '@/components/emcn'
+import type {
+ Recurrence,
+ RecurrenceFrequency,
+} from '@/app/workspace/[workspaceId]/scheduled-tasks/utils/recurrence'
+
+const WEEKDAY_PRESET = [1, 2, 3, 4, 5]
+/** Seed count when the user first chooses "ends after N runs". */
+const DEFAULT_END_AFTER_COUNT = 10
+/** Cadence a task falls back to when the user first flips on recurrence. */
+const DEFAULT_RECURRING_FREQUENCY = 'daily'
+
+/** The frequency presets the dropdown authors, keyed by a synthetic option value. */
+type FrequencyOption = 'daily' | 'weekly' | 'weekdays' | 'monthly' | 'custom'
+
+function isWeekdayPreset(weekdays: number[]): boolean {
+ return (
+ weekdays.length === WEEKDAY_PRESET.length && WEEKDAY_PRESET.every((d) => weekdays.includes(d))
+ )
+}
+
+/** Collapses a recurring recurrence into the single dropdown value that represents it. */
+function frequencyOptionFor(recurrence: Recurrence): FrequencyOption {
+ if (recurrence.frequency === 'weekly')
+ return isWeekdayPreset(recurrence.weekdays) ? 'weekdays' : 'weekly'
+ // Exhaustiveness fallback: callers gate on `isRecurring`, so `once` never
+ // reaches here at runtime, but the dropdown can't represent it — mapping it to
+ // a recurring default keeps the return type `FrequencyOption` without a cast.
+ if (recurrence.frequency === 'once') return DEFAULT_RECURRING_FREQUENCY
+ return recurrence.frequency
+}
+
+interface RecurrenceSectionProps {
+ recurrence: Recurrence
+ onChange: (recurrence: Recurrence) => void
+ /** The launch day, so weekly/monthly labels name the weekday and day-of-month. */
+ launchDate: string
+}
+
+/**
+ * The repeat + end controls for a scheduled task, rendered as a body section
+ * below the prompt: a "Recurring" {@link Switch} that toggles a one-time launch
+ * into a repeat, and — once on — the frequency preset and how it ends (never, on
+ * a date, or after N runs).
+ *
+ * Composed as a sibling between the prompt body and footer; it owns its own
+ * leading separator and mirrors {@link ChipModalBody}'s spacing
+ * (`gap-4 px-2 pt-4 pb-4.5`) so every {@link ChipModalField} lands at the same
+ * effective `px-4` as the modal header/footer — no changes to the `ChipModal`
+ * primitives.
+ */
+export function RecurrenceSection({ recurrence, onChange, launchDate }: RecurrenceSectionProps) {
+ /**
+ * The cadence to reinstate when recurrence is toggled back on. Toggling off
+ * collapses `frequency` to `once`, dropping which preset was active, so the
+ * last recurring cadence is cached here and restored — a paused "Weekly on
+ * Mon" returns as weekly, not silently reset to daily. Written during render
+ * (an idempotent cache), so it is current before the toggle handler reads it.
+ */
+ const lastRecurringFrequency = useRef(DEFAULT_RECURRING_FREQUENCY)
+ if (recurrence.frequency !== 'once') lastRecurringFrequency.current = recurrence.frequency
+
+ const launch = new Date(`${launchDate}T00:00`)
+ const isRecurring = recurrence.frequency !== 'once'
+
+ const frequencyOptions = [
+ { value: 'daily', label: 'Daily' },
+ { value: 'weekly', label: `Weekly on ${format(launch, 'EEE')}` },
+ { value: 'weekdays', label: 'Weekdays' },
+ { value: 'monthly', label: `Monthly on the ${format(launch, 'do')}` },
+ ...(recurrence.frequency === 'custom' ? [{ value: 'custom', label: 'Custom' }] : []),
+ ]
+
+ /**
+ * Flips the one-time launch into a repeat and back. Toggling off keeps the
+ * recurrence shape (weekdays, end, and a passed-through `custom` cron) on the
+ * object and only collapses `frequency` to `once`; toggling back on reinstates
+ * the remembered cadence, so neither a weekly preset nor a conversationally
+ * authored custom cron is silently rewritten to daily.
+ */
+ const handleRecurringToggle = (checked: boolean) => {
+ onChange({ ...recurrence, frequency: checked ? lastRecurringFrequency.current : 'once' })
+ }
+
+ const handleFrequencyChange = (value: string) => {
+ const option = value as FrequencyOption
+ switch (option) {
+ case 'daily':
+ onChange({ ...recurrence, frequency: 'daily', weekdays: [], cron: undefined })
+ return
+ case 'weekly':
+ onChange({
+ ...recurrence,
+ frequency: 'weekly',
+ weekdays: [launch.getDay()],
+ cron: undefined,
+ })
+ return
+ case 'weekdays':
+ onChange({
+ ...recurrence,
+ frequency: 'weekly',
+ weekdays: [...WEEKDAY_PRESET],
+ cron: undefined,
+ })
+ return
+ case 'monthly':
+ onChange({ ...recurrence, frequency: 'monthly', weekdays: [], cron: undefined })
+ return
+ case 'custom':
+ onChange({ ...recurrence, frequency: 'custom' })
+ }
+ }
+
+ const handleEndChange = (value: string) => {
+ if (value === 'never') onChange({ ...recurrence, end: { type: 'never' } })
+ else if (value === 'on')
+ onChange({ ...recurrence, end: { type: 'on', date: format(launch, 'yyyy-MM-dd') } })
+ else {
+ const count = recurrence.end.type === 'after' ? recurrence.end.count : DEFAULT_END_AFTER_COUNT
+ onChange({ ...recurrence, end: { type: 'after', count } })
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ {isRecurring && (
+ <>
+
+
+
+
+ {recurrence.end.type === 'on' && (
+
+ onChange({ ...recurrence, end: { type: 'on', date } })}
+ fullWidth
+ />
+
+ )}
+
+ {recurrence.end.type === 'after' && (
+ {
+ const count = Math.max(1, Math.floor(Number(value) || 1))
+ onChange({ ...recurrence, end: { type: 'after', count } })
+ }}
+ />
+ )}
+ >
+ )}
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx
index 829423504f..0d05b31fdd 100644
--- a/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/task-modal.tsx
@@ -18,7 +18,7 @@ import {
PromptEditor,
usePromptEditor,
} from '@/app/workspace/[workspaceId]/home/components/user-input/components'
-import { RecurrenceControl } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-control'
+import { RecurrenceSection } from '@/app/workspace/[workspaceId]/scheduled-tasks/components/task-modal/recurrence-section'
import type { CalendarSlot } from '@/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-calendar'
import {
DEFAULT_RECURRENCE,
@@ -117,10 +117,10 @@ interface TaskModalProps {
/**
* The "schedule a task" modal, shared by create (blank, or pre-filled from a
- * duplicate) and edit (seeded from a task's schedule). The body is one prompt
- * surface — the chat input's editor, so `@` mentions resources and `/` invokes
- * skills exactly like talking to Sim — and the footer carries the recurrence,
- * launch date/time, and (edit only) Delete.
+ * duplicate) and edit (seeded from a task's schedule). The body is the chat
+ * input's editor — so `@` mentions resources and `/` invokes skills exactly like
+ * talking to Sim — followed by the recurrence section; the footer carries the
+ * launch date/time and (edit only) Delete.
*/
export function TaskModal({
open,
@@ -296,10 +296,11 @@ function TaskModalContent({
}
/**
- * Footer secondary actions. Delete is disabled while `submitting` because it
- * bypasses the dismiss guard — it closes the modal via `closeTask`, not the
- * guarded `onOpenChange` — so without the lock an in-flight edit and a delete
- * could run against the same task at once.
+ * Footer secondary actions — the launch date/time pickers and (edit only)
+ * Delete. Delete is disabled while `submitting` because it bypasses the
+ * dismiss guard — it closes the modal via `closeTask`, not the guarded
+ * `onOpenChange` — so without the lock an in-flight edit and a delete could
+ * run against the same task at once. Recurrence lives in the body, not here.
*/
const secondaryActions: ChipModalFooterSlotAction[] = [
...(edit && onRequestDelete
@@ -312,15 +313,6 @@ function TaskModalContent({
},
]
: []),
- {
- custom: (
-
- ),
- },
{ custom: },
{ custom: },
]
@@ -338,6 +330,7 @@ function TaskModalContent({
onSubmit={handleSubmit}
/>
+
}
diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts
index 96f730c564..ae022fe63b 100644
--- a/apps/sim/components/emcn/components/index.ts
+++ b/apps/sim/components/emcn/components/index.ts
@@ -53,6 +53,7 @@ export {
ChipModalPromptBody,
type ChipModalPromptBodyProps,
type ChipModalProps,
+ ChipModalSeparator,
type ChipModalTab,
ChipModalTabs,
type ChipModalTabsProps,