mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 20:53:15 +00:00
* feat: add scheduled web agents Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> * fix: secure scheduled agent repositories Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> * feat: rebuild scheduled agents as Automations tab Scrap the inline ScheduledAgentsPanel and replace it with a dedicated Automations tab: sidebar nav entry, list view with stat cards + empty state, and a full editor (name, Active toggle, repo, scheduled trigger picker, agent instructions + model). * fix: clear collapsed-sidebar button on mobile in Automations * fix: allow clearing automation repo on update --------- Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com>
78 lines
2.7 KiB
TypeScript
78 lines
2.7 KiB
TypeScript
import { useEffect, useRef, useState } from "react"
|
|
import { CaretRightIcon, ClockIcon, PlusIcon } from "@phosphor-icons/react"
|
|
|
|
import { CRON_PRESETS } from "@/lib/agents/cron"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface ScheduleTriggerPickerProps {
|
|
/** Called with a cron value for a preset, or null when the user picks Custom. */
|
|
onSelect: (cron: string | null) => void
|
|
triggerLabel?: string
|
|
}
|
|
|
|
const OPTIONS: Array<{ id: string; label: string; cron: string | null }> = [
|
|
...CRON_PRESETS.map((preset) => ({
|
|
id: preset.id,
|
|
label: preset.label,
|
|
cron: preset.value,
|
|
})),
|
|
{ id: "custom", label: "Custom (cron)", cron: null },
|
|
]
|
|
|
|
export function ScheduleTriggerPicker({
|
|
onSelect,
|
|
triggerLabel = "Add Trigger",
|
|
}: ScheduleTriggerPickerProps) {
|
|
const [open, setOpen] = useState(false)
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(e: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false)
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handleClickOutside)
|
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
|
}, [])
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen((value) => !value)}
|
|
className="flex w-full items-center gap-2 rounded-lg px-3 py-2.5 text-sm text-[var(--ui-text-muted)] transition-colors hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
|
>
|
|
<PlusIcon className="size-4" />
|
|
{triggerLabel}
|
|
</button>
|
|
|
|
{open && (
|
|
<div className="absolute top-full left-0 z-50 mt-1 w-72 overflow-hidden rounded-xl border border-[var(--ui-border)] bg-[var(--ui-surface)] py-1 shadow-lg">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 text-[11px] font-semibold tracking-wide text-[var(--ui-text-dim)] uppercase">
|
|
<ClockIcon className="size-3.5" />
|
|
Scheduled
|
|
</div>
|
|
{OPTIONS.map((option) => (
|
|
<button
|
|
key={option.id}
|
|
type="button"
|
|
onClick={() => {
|
|
onSelect(option.cron)
|
|
setOpen(false)
|
|
}}
|
|
className={cn(
|
|
"flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-[var(--ui-text-muted)] transition-colors hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
|
)}
|
|
>
|
|
<span className="flex-1">{option.label}</span>
|
|
{option.cron === null && (
|
|
<CaretRightIcon className="size-3.5 opacity-50" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|