/ src / components / schedules / schedule-sheet.tsx
schedule-sheet.tsx
  1  'use client'
  2  
  3  import { useEffect, useState, useMemo } from 'react'
  4  import { useAppStore } from '@/stores/use-app-store'
  5  import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules/schedules'
  6  import { BottomSheet } from '@/components/shared/bottom-sheet'
  7  import { AgentPickerList } from '@/components/shared/agent-picker-list'
  8  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
  9  import { inputClass } from '@/components/shared/form-styles'
 10  import { AgentAvatar } from '@/components/agents/agent-avatar'
 11  import type { ScheduleTaskMode, ScheduleType, ScheduleStatus } from '@/types'
 12  import cronstrue from 'cronstrue'
 13  import { SectionLabel } from '@/components/shared/section-label'
 14  import { SCHEDULE_TEMPLATES, type ScheduleTemplate } from '@/lib/schedules/schedule-templates'
 15  import { HintTip } from '@/components/shared/hint-tip'
 16  import { isUserCreatedSchedule } from '@/lib/schedules/schedule-origin'
 17  import { toast } from 'sonner'
 18  import {
 19    Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
 20    Activity, ShieldCheck, DatabaseBackup, FileText,
 21  } from 'lucide-react'
 22  
 23  const TEMPLATE_ICONS: Record<string, React.ComponentType<{ className?: string; size?: number }>> = {
 24    Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
 25    Activity, ShieldCheck, DatabaseBackup, FileText,
 26  }
 27  
 28  const CRON_PRESETS = [
 29    { label: 'Every hour', cron: '0 * * * *' },
 30    { label: 'Every 6 hours', cron: '0 */6 * * *' },
 31    { label: 'Daily at 9am', cron: '0 9 * * *' },
 32    { label: 'Weekly Mon 9am', cron: '0 9 * * 1' },
 33  ]
 34  
 35  async function getNextRunsAsync(cron: string, count: number = 3): Promise<Date[]> {
 36    try {
 37      const { CronExpressionParser } = await import('cron-parser')
 38      const interval = CronExpressionParser.parse(cron)
 39      const runs: Date[] = []
 40      for (let i = 0; i < count; i++) {
 41        runs.push(interval.next().toDate())
 42      }
 43      return runs
 44    } catch {
 45      return []
 46    }
 47  }
 48  
 49  function formatCronHuman(cron: string): string {
 50    try {
 51      return cronstrue.toString(cron, { use24HourTimeFormat: false })
 52    } catch {
 53      return 'Invalid cron expression'
 54    }
 55  }
 56  
 57  function formatDate(d: Date): string {
 58    return d.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }) +
 59      ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
 60  }
 61  
 62  const STEPS_CREATE = ['Template', 'What', 'When', 'Review'] as const
 63  const STEPS_EDIT = ['What', 'When', 'Review'] as const
 64  type Step = 0 | 1 | 2 | 3
 65  
 66  function applyTemplate(
 67    tpl: ScheduleTemplate,
 68    setters: {
 69      setName: (v: string) => void
 70      setTaskPrompt: (v: string) => void
 71      setScheduleType: (v: ScheduleType) => void
 72      setCron: (v: string) => void
 73      setIntervalMs: (v: number) => void
 74      setCustomCron: (v: boolean) => void
 75    },
 76  ) {
 77    setters.setName(tpl.name)
 78    setters.setTaskPrompt(tpl.defaults.taskPrompt)
 79    setters.setScheduleType(tpl.defaults.scheduleType)
 80    if (tpl.defaults.cron) {
 81      setters.setCron(tpl.defaults.cron)
 82      setters.setCustomCron(!CRON_PRESETS.some((p) => p.cron === tpl.defaults.cron))
 83    }
 84    if (tpl.defaults.intervalMs) setters.setIntervalMs(tpl.defaults.intervalMs)
 85  }
 86  
 87  export function ScheduleSheet() {
 88    const open = useAppStore((s) => s.scheduleSheetOpen)
 89    const setOpen = useAppStore((s) => s.setScheduleSheetOpen)
 90    const editingId = useAppStore((s) => s.editingScheduleId)
 91    const setEditingId = useAppStore((s) => s.setEditingScheduleId)
 92    const schedules = useAppStore((s) => s.schedules)
 93    const loadSchedules = useAppStore((s) => s.loadSchedules)
 94    const agents = useAppStore((s) => s.agents)
 95    const loadAgents = useAppStore((s) => s.loadAgents)
 96    const templatePrefill = useAppStore((s) => s.scheduleTemplatePrefill)
 97    const setTemplatePrefill = useAppStore((s) => s.setScheduleTemplatePrefill)
 98  
 99    const [step, setStep] = useState<Step>(0)
100    const [name, setName] = useState('')
101    const [agentId, setAgentId] = useState('')
102    const [taskPrompt, setTaskPrompt] = useState('')
103    const [scheduleType, setScheduleType] = useState<ScheduleType>('cron')
104    const [cron, setCron] = useState('0 * * * *')
105    const [intervalMs, setIntervalMs] = useState(3600000)
106    const [status, setStatus] = useState<ScheduleStatus>('active')
107    const [taskMode, setTaskMode] = useState<ScheduleTaskMode>('task')
108    const [message, setMessage] = useState('')
109    const [customCron, setCustomCron] = useState(false)
110    const [confirmDelete, setConfirmDelete] = useState(false)
111    const [deleting, setDeleting] = useState(false)
112  
113    const editing = editingId ? schedules[editingId] : null
114    const isCreating = !editing
115    const steps = isCreating ? STEPS_CREATE : STEPS_EDIT
116    const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
117  
118    // Compute which logical step we're on (template step only exists in create mode)
119    const templateStep = isCreating ? 0 : -1
120    const whatStep = isCreating ? 1 : 0
121    const whenStep = isCreating ? 2 : 1
122    const reviewStep = isCreating ? 3 : 2
123  
124    useEffect(() => {
125      if (open) {
126        loadAgents()
127        if (editing) {
128          setStep(0)
129          setName(editing.name || '')
130          setAgentId(editing.agentId)
131          setTaskPrompt(editing.taskPrompt)
132          setScheduleType(editing.scheduleType)
133          setCron(editing.cron || '0 * * * *')
134          setIntervalMs(editing.intervalMs || 3600000)
135          setStatus(editing.status)
136          setTaskMode(editing.taskMode === 'wake_only' ? 'wake_only' : editing.taskMode === 'protocol' ? 'protocol' : 'task')
137          setMessage(editing.message || '')
138          setCustomCron(!CRON_PRESETS.some((p) => p.cron === editing.cron))
139        } else if (templatePrefill) {
140          // Opened from a quick-start card with pre-filled values
141          setName(templatePrefill.name)
142          setTaskPrompt(templatePrefill.taskPrompt)
143          setScheduleType(templatePrefill.scheduleType)
144          if (templatePrefill.cron) {
145            setCron(templatePrefill.cron)
146            setCustomCron(!CRON_PRESETS.some((p) => p.cron === templatePrefill.cron))
147          }
148          if (templatePrefill.intervalMs) setIntervalMs(templatePrefill.intervalMs)
149          setAgentId('')
150          setStatus('active')
151          setStep(1) // Skip template picker, go to "What" step
152          setTemplatePrefill(null)
153        } else {
154          setStep(0) // Start at template picker
155          setName('')
156          setAgentId('')
157          setTaskPrompt('')
158          setScheduleType('cron')
159          setCron('0 * * * *')
160          setIntervalMs(3600000)
161          setStatus('active')
162          setTaskMode('task')
163          setMessage('')
164          setCustomCron(false)
165        }
166      }
167      // eslint-disable-next-line react-hooks/exhaustive-deps
168    }, [open, editingId])
169  
170    const cronHuman = useMemo(() => formatCronHuman(cron), [cron])
171    const [nextRuns, setNextRuns] = useState<Date[]>([])
172    useEffect(() => {
173      getNextRunsAsync(cron).then(setNextRuns)
174    }, [cron])
175  
176    const onClose = () => {
177      setConfirmDelete(false)
178      setDeleting(false)
179      setOpen(false)
180      setEditingId(null)
181    }
182  
183    const handleSave = async () => {
184      const data = {
185        name: name.trim(),
186        agentId,
187        taskPrompt: taskMode === 'wake_only' ? message : taskPrompt,
188        taskMode,
189        message: taskMode === 'task' ? undefined : message,
190        protocolTemplateId: taskMode === 'protocol' ? 'single_agent_structured_run' : undefined,
191        scheduleType,
192        cron: scheduleType === 'cron' ? cron : undefined,
193        intervalMs: scheduleType === 'interval' ? intervalMs : undefined,
194        runAt: scheduleType === 'once' ? Date.now() + intervalMs : undefined,
195        status,
196      }
197      try {
198        if (editing) {
199          await updateSchedule(editing.id, data)
200          toast.success('Schedule updated successfully')
201        } else {
202          await createSchedule(data)
203          toast.success('Schedule created successfully')
204        }
205        await loadSchedules()
206        onClose()
207      } catch (err: unknown) {
208        toast.error(err instanceof Error ? err.message : 'Failed to save schedule')
209      }
210    }
211  
212    const handleDelete = async () => {
213      if (!editing) return
214      setDeleting(true)
215      try {
216        await deleteSchedule(editing.id)
217        toast.success('Schedule archived')
218        await loadSchedules()
219        setConfirmDelete(false)
220        onClose()
221      } catch (err: unknown) {
222        toast.error(err instanceof Error ? err.message : 'Failed to delete schedule')
223      } finally {
224        setDeleting(false)
225      }
226    }
227  
228    // Step validation
229    const step0Valid = name.trim().length > 0 && agentId.length > 0 && (taskMode === 'wake_only' ? message.trim().length > 0 : taskPrompt.trim().length > 0)
230    const step1Valid = scheduleType === 'cron' ? cron.trim().length > 0 : intervalMs > 0
231  
232    const selectedAgent = agentId ? agents[agentId] : null
233    const creatorAgent = editing?.createdByAgentId ? agents[editing.createdByAgentId] : null
234  
235    return (
236      <BottomSheet open={open} onClose={onClose} wide>
237        <div className="mb-8">
238          <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
239            {editing ? 'Edit Schedule' : 'New Schedule'}
240          </h2>
241          <p className="text-[14px] text-text-3">Automate agent tasks on a schedule</p>
242        </div>
243  
244        {/* Step indicator */}
245        <div className="flex items-center gap-2 mb-10">
246          {steps.map((label, i) => (
247            <div key={label} className="flex items-center gap-2">
248              {i > 0 && <div className={`w-8 h-px ${i <= step ? 'bg-accent-bright/40' : 'bg-white/[0.06]'}`} />}
249              <button
250                onClick={() => {
251                  if (i < step) setStep(i as Step)
252                  else if (i === step + 1) {
253                    if (step === whatStep && step0Valid) setStep(i as Step)
254                    else if (step === whenStep && step1Valid) setStep(i as Step)
255                    else if (step === templateStep) setStep(i as Step)
256                  }
257                }}
258                className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border-none
259                  ${i === step
260                    ? 'bg-accent-soft text-accent-bright'
261                    : i < step
262                      ? 'bg-white/[0.04] text-text-2'
263                      : 'bg-transparent text-text-3/50'}`}
264                style={{ fontFamily: 'inherit' }}
265              >
266                <span className={`w-5 h-5 rounded-full text-[10px] font-700 flex items-center justify-center
267                  ${i === step
268                    ? 'bg-accent-bright text-white'
269                    : i < step
270                      ? 'bg-emerald-400/20 text-emerald-400'
271                      : 'bg-white/[0.06] text-text-3/50'}`}>
272                  {i < step ? (
273                    <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
274                  ) : (
275                    i + 1
276                  )}
277                </span>
278                {label}
279              </button>
280            </div>
281          ))}
282        </div>
283  
284        {/* Template Picker (create only) */}
285        {step === templateStep && isCreating && (
286          <div>
287            <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
288              {SCHEDULE_TEMPLATES.map((tpl) => {
289                const IconComp = TEMPLATE_ICONS[tpl.icon] || FileText
290                return (
291                  <button
292                    key={tpl.id}
293                    onClick={() => {
294                      const setters = { setName, setTaskPrompt, setScheduleType, setCron, setIntervalMs, setCustomCron }
295                      applyTemplate(tpl, setters)
296                      setStep(whatStep as Step)
297                    }}
298                    className="flex items-start gap-3.5 p-4 rounded-[14px] border border-white/[0.06] bg-surface
299                      text-left cursor-pointer transition-all duration-200 hover:bg-surface-2 hover:border-white/[0.1]
300                      active:scale-[0.98]"
301                    style={{ fontFamily: 'inherit' }}
302                  >
303                    <div className="w-9 h-9 rounded-[10px] bg-accent-soft flex items-center justify-center shrink-0 mt-0.5">
304                      <IconComp size={16} className="text-accent-bright" />
305                    </div>
306                    <div className="min-w-0">
307                      <div className="text-[14px] font-600 text-text mb-0.5">{tpl.name}</div>
308                      <div className="text-[12px] text-text-3/70 leading-[1.4]">{tpl.description}</div>
309                      <div className="mt-1.5 text-[11px] text-text-3/40 capitalize">{tpl.category}</div>
310                    </div>
311                  </button>
312                )
313              })}
314            </div>
315            <button
316              onClick={() => setStep(whatStep as Step)}
317              className="w-full py-3.5 rounded-[14px] border border-dashed border-white/[0.08] bg-transparent
318                text-text-3 text-[14px] font-600 cursor-pointer transition-all hover:bg-surface hover:text-text-2 hover:border-white/[0.12]"
319              style={{ fontFamily: 'inherit' }}
320            >
321              Start from scratch
322            </button>
323          </div>
324        )}
325  
326        {/* Step: What */}
327        {step === whatStep && (
328          <div>
329            <div className="mb-8">
330              <SectionLabel>Name</SectionLabel>
331              <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
332            </div>
333  
334            <div className="mb-8">
335              <SectionLabel>Agent</SectionLabel>
336              <AgentPickerList
337                agents={agentList}
338                selected={agentId}
339                onSelect={(id) => setAgentId(id)}
340                showDelegationBadge={true}
341              />
342            </div>
343  
344            <div className="mb-8">
345              <div className="flex items-center gap-2 mb-3">
346                <SectionLabel className="mb-0">Task Mode</SectionLabel>
347                <HintTip text="Create task: creates a board task. Wake agent only: sends a message without a task. Structured session: launches a bounded structured run instead of a normal task." />
348              </div>
349              <div className="grid grid-cols-3 gap-3">
350                <button
351                  onClick={() => setTaskMode('task')}
352                  className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
353                    active:scale-[0.97] text-[14px] font-600 border
354                    ${taskMode === 'task'
355                      ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
356                      : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
357                  style={{ fontFamily: 'inherit' }}
358                >
359                  Create task
360                </button>
361                <button
362                  onClick={() => setTaskMode('wake_only')}
363                  className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
364                    active:scale-[0.97] text-[14px] font-600 border
365                    ${taskMode === 'wake_only'
366                      ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
367                      : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
368                  style={{ fontFamily: 'inherit' }}
369                >
370                  Wake agent only
371                </button>
372                <button
373                  onClick={() => setTaskMode('protocol')}
374                  className={`py-3 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
375                    active:scale-[0.97] text-[14px] font-600 border
376                    ${taskMode === 'protocol'
377                      ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
378                      : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
379                  style={{ fontFamily: 'inherit' }}
380                >
381                  Structured session
382                </button>
383              </div>
384            </div>
385  
386            {taskMode === 'wake_only' ? (
387              <div className="mb-8">
388                <SectionLabel>Wake Message</SectionLabel>
389                <textarea
390                  value={message}
391                  onChange={(e) => setMessage(e.target.value)}
392                  placeholder="Message to send to the agent when woken"
393                  rows={4}
394                  className={`${inputClass} resize-y min-h-[100px]`}
395                  style={{ fontFamily: 'inherit' }}
396                />
397              </div>
398            ) : taskMode === 'protocol' ? (
399              <>
400                <div className="mb-4">
401                  <SectionLabel>Structured Session Goal</SectionLabel>
402                  <textarea
403                    value={taskPrompt}
404                    onChange={(e) => setTaskPrompt(e.target.value)}
405                    placeholder="What should this structured session accomplish?"
406                    rows={4}
407                    className={`${inputClass} resize-y min-h-[100px]`}
408                    style={{ fontFamily: 'inherit' }}
409                  />
410                </div>
411                <div className="mb-8">
412                  <SectionLabel>Kickoff Context</SectionLabel>
413                  <textarea
414                    value={message}
415                    onChange={(e) => setMessage(e.target.value)}
416                    placeholder="Optional context for the structured session run"
417                    rows={3}
418                    className={`${inputClass} resize-y min-h-[88px]`}
419                    style={{ fontFamily: 'inherit' }}
420                  />
421                </div>
422              </>
423            ) : (
424              <div className="mb-8">
425                <SectionLabel>Task Prompt</SectionLabel>
426                <textarea
427                  value={taskPrompt}
428                  onChange={(e) => setTaskPrompt(e.target.value)}
429                  placeholder="What should the agent do when triggered?"
430                  rows={4}
431                  className={`${inputClass} resize-y min-h-[100px]`}
432                  style={{ fontFamily: 'inherit' }}
433                />
434              </div>
435            )}
436          </div>
437        )}
438  
439        {/* Step: When */}
440        {step === whenStep && (
441          <div>
442            <div className="mb-8">
443              <div className="flex items-center gap-2 mb-3">
444                <SectionLabel className="mb-0">Schedule Type</SectionLabel>
445                <HintTip text="Once: runs a single time. Interval: repeats every N minutes. Cron: advanced scheduling with cron syntax" />
446              </div>
447              <div className="grid grid-cols-3 gap-3">
448                {(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
449                  <button
450                    key={t}
451                    onClick={() => setScheduleType(t)}
452                    className={`py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
453                      active:scale-[0.97] text-[14px] font-600 capitalize border
454                      ${scheduleType === t
455                        ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
456                        : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
457                    style={{ fontFamily: 'inherit' }}
458                  >
459                    {t}
460                  </button>
461                ))}
462              </div>
463            </div>
464  
465            {scheduleType === 'cron' && (
466              <div className="mb-8">
467                <div className="flex items-center gap-2 mb-3">
468                  <SectionLabel className="mb-0">Schedule</SectionLabel>
469                  <HintTip text="Standard cron format: minute hour day month weekday (e.g. 0 9 * * 1-5 = weekdays at 9am)" />
470                </div>
471  
472                {/* Preset buttons */}
473                <div className="flex flex-wrap gap-2 mb-4">
474                  {CRON_PRESETS.map((p) => (
475                    <button
476                      key={p.cron}
477                      onClick={() => { setCron(p.cron); setCustomCron(false) }}
478                      className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
479                        ${cron === p.cron && !customCron
480                          ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
481                          : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
482                      style={{ fontFamily: 'inherit' }}
483                    >
484                      {p.label}
485                    </button>
486                  ))}
487                  <button
488                    onClick={() => setCustomCron(true)}
489                    className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
490                      ${customCron
491                        ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
492                        : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
493                    style={{ fontFamily: 'inherit' }}
494                  >
495                    Custom
496                  </button>
497                </div>
498  
499                {/* Custom cron input */}
500                {customCron && (
501                  <input type="text" value={cron} onChange={(e) => setCron(e.target.value)} placeholder="0 * * * *" className={`${inputClass} font-mono text-[14px] mb-3`} />
502                )}
503  
504                {/* Human-readable preview */}
505                <div className="p-4 rounded-[14px] bg-surface border border-white/[0.06]">
506                  <div className="text-[14px] text-text-2 font-600 mb-2">{cronHuman}</div>
507                  {cron && (
508                    <div className="font-mono text-[12px] text-text-3/50 mb-3">{cron}</div>
509                  )}
510                  {nextRuns.length > 0 && (
511                    <div className="space-y-1.5">
512                      <div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600">Next runs</div>
513                      {nextRuns.map((d, i) => (
514                        <div key={i} className="text-[12px] text-text-3 font-mono">{formatDate(d)}</div>
515                      ))}
516                    </div>
517                  )}
518                </div>
519              </div>
520            )}
521  
522            {scheduleType === 'interval' && (
523              <div className="mb-8">
524                <SectionLabel>Interval (minutes)</SectionLabel>
525                <input
526                  type="number"
527                  value={Math.round(intervalMs / 60000)}
528                  onChange={(e) => setIntervalMs(Math.max(1, parseInt(e.target.value) || 1) * 60000)}
529                  className={inputClass}
530                  style={{ fontFamily: 'inherit' }}
531                />
532              </div>
533            )}
534  
535            {editing && (
536              <div className="mb-8">
537                <SectionLabel>Status</SectionLabel>
538                <div className="flex gap-2">
539                  {(['active', 'paused'] as ScheduleStatus[]).map((s) => (
540                    <button
541                      key={s}
542                      onClick={() => setStatus(s)}
543                      className={`px-4 py-2 rounded-[10px] text-[13px] font-600 capitalize cursor-pointer transition-all border
544                        ${status === s
545                          ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
546                          : 'bg-surface border-white/[0.06] text-text-3'}`}
547                      style={{ fontFamily: 'inherit' }}
548                    >
549                      {s}
550                    </button>
551                  ))}
552                </div>
553              </div>
554            )}
555          </div>
556        )}
557  
558        {/* Step: Review */}
559        {step === reviewStep && (
560          <div className="mb-8">
561            <div className="p-5 rounded-[16px] bg-surface border border-white/[0.06] space-y-4">
562              <div>
563                <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Name</span>
564                <div className="text-[14px] text-text font-600 mt-0.5">{name}</div>
565              </div>
566              <div>
567                <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Agent</span>
568                <div className="text-[14px] text-text font-600 mt-0.5">{selectedAgent?.name || agentId}</div>
569              </div>
570              {editing && (
571                <div>
572                  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Created By</span>
573                  {creatorAgent ? (
574                    <div className="mt-1 inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] px-3 py-2 text-[13px] text-text-2">
575                      <AgentAvatar
576                        seed={creatorAgent.avatarSeed}
577                        avatarUrl={creatorAgent.avatarUrl}
578                        name={creatorAgent.name}
579                        size={18}
580                      />
581                      <span>{creatorAgent.name}</span>
582                    </div>
583                  ) : (
584                    <div className="text-[13px] text-text-2 mt-0.5">
585                      {isUserCreatedSchedule(editing) ? 'Manual / user-created' : 'Unknown'}
586                    </div>
587                  )}
588                </div>
589              )}
590              <div>
591                <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Mode</span>
592                <div className="text-[14px] text-text font-600 mt-0.5">{taskMode === 'wake_only' ? 'Wake agent only' : taskMode === 'protocol' ? 'Structured session' : 'Create task'}</div>
593              </div>
594              <div>
595                <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">{taskMode === 'wake_only' ? 'Wake Message' : taskMode === 'protocol' ? 'Session Goal' : 'Task'}</span>
596                <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskMode === 'wake_only' ? message : taskPrompt}</div>
597              </div>
598              {taskMode === 'protocol' && message.trim() && (
599                <div>
600                  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Kickoff Context</span>
601                  <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{message}</div>
602                </div>
603              )}
604              {taskMode === 'protocol' && (
605                <div>
606                  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Template</span>
607                  <div className="text-[13px] text-text-2 mt-0.5">Single-agent structured run</div>
608                </div>
609              )}
610              <div className="h-px bg-white/[0.06]" />
611              <div>
612                <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Schedule</span>
613                <div className="text-[14px] text-text font-600 mt-0.5 capitalize">{scheduleType}</div>
614                {scheduleType === 'cron' && (
615                  <div className="text-[12px] text-text-3 font-mono mt-0.5">{cronHuman} ({cron})</div>
616                )}
617                {scheduleType === 'interval' && (
618                  <div className="text-[12px] text-text-3 font-mono mt-0.5">Every {Math.round(intervalMs / 60000)} minutes</div>
619                )}
620                {scheduleType === 'once' && (
621                  <div className="text-[12px] text-text-3 font-mono mt-0.5">Run once</div>
622                )}
623              </div>
624              {editing && (
625                <div>
626                  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Status</span>
627                  <div className="text-[14px] text-text font-600 mt-0.5 capitalize">{status}</div>
628                </div>
629              )}
630            </div>
631          </div>
632        )}
633  
634        {/* Footer */}
635        <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
636          {editing && step === 0 && (
637            <button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
638              Archive
639            </button>
640          )}
641          {step > (isCreating ? templateStep : 0) && step !== templateStep && (
642            <button
643              onClick={() => setStep((step - 1) as Step)}
644              className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
645              style={{ fontFamily: 'inherit' }}
646            >
647              Back
648            </button>
649          )}
650          <div className="flex-1" />
651          {step !== templateStep && (
652            <button
653              onClick={onClose}
654              className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
655              style={{ fontFamily: 'inherit' }}
656            >
657              Cancel
658            </button>
659          )}
660          {step === templateStep && isCreating ? (
661            <button
662              onClick={onClose}
663              className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
664              style={{ fontFamily: 'inherit' }}
665            >
666              Cancel
667            </button>
668          ) : step < reviewStep ? (
669            <button
670              onClick={() => setStep((step + 1) as Step)}
671              disabled={step === whatStep ? !step0Valid : !step1Valid}
672              className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
673              style={{ fontFamily: 'inherit' }}
674            >
675              Next
676            </button>
677          ) : (
678            <button
679              onClick={handleSave}
680              className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
681              style={{ fontFamily: 'inherit' }}
682            >
683              {editing ? 'Save' : 'Create'}
684            </button>
685          )}
686        </div>
687        <ConfirmDialog
688          open={confirmDelete}
689          title="Archive Schedule?"
690          message={editing ? `Archive "${editing.name}"? Future runs will stop and any in-flight scheduled task will be cancelled.` : 'Archive this schedule?'}
691          confirmLabel={deleting ? 'Archiving...' : 'Archive'}
692          confirmDisabled={deleting}
693          cancelDisabled={deleting}
694          danger
695          onConfirm={() => { void handleDelete() }}
696          onCancel={() => { if (!deleting) setConfirmDelete(false) }}
697        />
698      </BottomSheet>
699    )
700  }