diff --git a/ui/src/components/agents/AutomationEditor.tsx b/ui/src/components/agents/AutomationEditor.tsx index b9ba060b..fe256712 100644 --- a/ui/src/components/agents/AutomationEditor.tsx +++ b/ui/src/components/agents/AutomationEditor.tsx @@ -9,12 +9,13 @@ import { import type { ModelOption } from "@/lib/api" import type { AgentSchedule } from "@/lib/agents/types" +import type { AutomationTemplate } from "@/lib/agents/automation-templates" import type { ModelSelection } from "@/lib/agents/provider/useModelOptions" import { RepoSelector } from "@/components/agents/RepoSelector" import { ScheduleTriggerPicker } from "@/components/agents/ScheduleTriggerPicker" import { Button } from "@/components/ui/button" import { Switch } from "@/components/ui/switch" -import { describeCron, presetForCron } from "@/lib/agents/cron" +import { describeCron, isDescribableCron } from "@/lib/agents/cron" import { useCreateAgentSchedule, useDeleteAgentSchedule, @@ -30,6 +31,8 @@ import { cn } from "@/lib/utils" interface AutomationEditorProps { mode: "create" | "edit" schedule?: AgentSchedule + /** Seeds the form in create mode when the user starts from a template. */ + template?: AutomationTemplate } function scheduleToSelection( @@ -46,7 +49,11 @@ function scheduleToSelection( : null } -export function AutomationEditor({ mode, schedule }: AutomationEditorProps) { +export function AutomationEditor({ + mode, + schedule, + template, +}: AutomationEditorProps) { const navigate = useNavigate() const reposQuery = useRepos() const { models, defaultSelection } = useModelOptions() @@ -55,11 +62,14 @@ export function AutomationEditor({ mode, schedule }: AutomationEditorProps) { const updateSchedule = useUpdateAgentSchedule() const deleteSchedule = useDeleteAgentSchedule() - const [name, setName] = useState(schedule?.name ?? "") - const [prompt, setPrompt] = useState(schedule?.prompt ?? "") - const [cron, setCron] = useState(schedule?.schedule ?? null) + const initialCron = schedule?.schedule ?? template?.schedule ?? null + const [name, setName] = useState(schedule?.name ?? template?.name ?? "") + const [prompt, setPrompt] = useState( + schedule?.prompt ?? template?.prompt ?? "" + ) + const [cron, setCron] = useState(initialCron) const [customMode, setCustomMode] = useState( - schedule ? presetForCron(schedule.schedule) === "custom" : false + initialCron ? !isDescribableCron(initialCron) : false ) const [repo, setRepo] = useState(schedule?.repo ?? null) const [enabled, setEnabled] = useState(schedule?.enabled ?? true) diff --git a/ui/src/components/agents/AutomationTemplates.tsx b/ui/src/components/agents/AutomationTemplates.tsx new file mode 100644 index 00000000..e1c52f47 --- /dev/null +++ b/ui/src/components/agents/AutomationTemplates.tsx @@ -0,0 +1,45 @@ +import { Link } from "@tanstack/react-router" +import { ClockIcon } from "@phosphor-icons/react" + +import { AUTOMATION_TEMPLATES } from "@/lib/agents/automation-templates" +import { describeCron } from "@/lib/agents/cron" + +export function AutomationTemplates() { + return ( +
+

+ Start from a template +

+

+ Prefilled instructions and a schedule you can tweak before saving. +

+
+ {AUTOMATION_TEMPLATES.map((template) => { + const Icon = template.icon + return ( + +
+ + + {template.name} + +
+

+ {template.description} +

+ + + {describeCron(template.schedule)} + + + ) + })} +
+
+ ) +} diff --git a/ui/src/components/agents/AutomationsList.tsx b/ui/src/components/agents/AutomationsList.tsx index 239485e4..0a8bd99f 100644 --- a/ui/src/components/agents/AutomationsList.tsx +++ b/ui/src/components/agents/AutomationsList.tsx @@ -9,6 +9,7 @@ import { } from "@phosphor-icons/react" import type { AgentSchedule } from "@/lib/agents/types" +import { AutomationTemplates } from "@/components/agents/AutomationTemplates" import { buttonVariants } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { describeCron } from "@/lib/agents/cron" @@ -83,6 +84,8 @@ export function AutomationsList() { )} + + ) diff --git a/ui/src/lib/agents/automation-templates.ts b/ui/src/lib/agents/automation-templates.ts new file mode 100644 index 00000000..31981c05 --- /dev/null +++ b/ui/src/lib/agents/automation-templates.ts @@ -0,0 +1,94 @@ +import { + BookOpenIcon, + BugIcon, + FlaskIcon, + GitPullRequestIcon, + NotePencilIcon, + PackageIcon, + ShieldCheckIcon, +} from "@phosphor-icons/react" +import type { Icon } from "@phosphor-icons/react" + +export interface AutomationTemplate { + /** Stable id used as the `?template=` search param on the new-automation route. */ + id: string + name: string + description: string + /** Seed instructions sent to Open SWE on each scheduled run. */ + prompt: string + /** Default 5-field cron expression (UTC). */ + schedule: string + icon: Icon +} + +export const AUTOMATION_TEMPLATES: ReadonlyArray = [ + { + id: "pr-review-digest", + name: "PR review digest", + description: + "Summarize open pull requests, review status, and what needs attention.", + schedule: "0 9 * * 1-5", + icon: GitPullRequestIcon, + prompt: `List the open pull requests on this repository and produce a concise review digest. For each PR include the title, author, age, whether CI is passing, review status, and any merge conflicts. Group them into "Needs review", "Changes requested", and "Ready to merge". Call out anything that has been waiting more than two days. Post the digest as a comment on the most relevant tracking issue, or summarize it in your final reply if there is none.`, + }, + { + id: "issue-triage", + name: "Issue triage", + description: + "Review newly opened issues, label them, and flag likely duplicates.", + schedule: "30 8 * * 1-5", + icon: BugIcon, + prompt: `Review issues opened on this repository since the last run. For each one, summarize the report, suggest appropriate labels (bug, feature, question, etc.), and identify likely duplicates by searching existing issues. Leave a short triage comment on each new issue with your assessment and, where confident, apply labels. End with a summary of what you triaged.`, + }, + { + id: "dependency-updates", + name: "Dependency update check", + description: + "Scan for outdated packages, security patches, and breaking changes.", + schedule: "0 9 * * 1", + icon: PackageIcon, + prompt: `Scan this repository's dependency manifests and lockfiles for outdated packages and known security advisories. Prioritize security patches and safe minor/patch upgrades, and note any major upgrades that may contain breaking changes. Open a draft pull request that bumps the low-risk, well-tested upgrades, and summarize the riskier ones for manual review.`, + }, + { + id: "flaky-test-tracker", + name: "Flaky test tracker", + description: + "Find tests that pass and fail intermittently across recent CI runs.", + schedule: "0 9 * * 1", + icon: FlaskIcon, + prompt: `Inspect recent CI runs for this repository and identify tests that have both passed and failed on the same or similar commits — likely flaky tests. For each suspect, note the test name, how often it failed, and any common error output. Open an issue (or update an existing tracking issue) listing the flaky tests ranked by failure frequency, with links to the relevant runs.`, + }, + { + id: "release-notes", + name: "Release notes drafter", + description: + "Draft user-facing release notes from pull requests merged recently.", + schedule: "0 16 * * 5", + icon: NotePencilIcon, + prompt: `Gather the pull requests merged into the default branch since the last release (or in the past week). Draft concise, user-facing release notes grouped into Features, Fixes, and Maintenance, written for end users rather than contributors. Include PR numbers for traceability and post the draft in your final reply.`, + }, + { + id: "docs-freshness", + name: "Docs freshness check", + description: + "Flag documentation that has drifted out of sync with recent code changes.", + schedule: "0 9 * * 3", + icon: BookOpenIcon, + prompt: `Compare recent code changes against this repository's documentation (README, docs/, and inline guides). Identify documentation that is stale, references removed APIs, or omits newly added behavior. Open a draft pull request with focused fixes for the clear-cut cases, and summarize anything ambiguous that needs a human decision.`, + }, + { + id: "security-audit", + name: "Security audit", + description: "Scan the codebase for hardcoded secrets and risky patterns.", + schedule: "0 7 * * 1", + icon: ShieldCheckIcon, + prompt: `Audit this repository for security issues: hardcoded secrets or credentials, unsafe handling of user input, overly permissive configuration, and dependencies with known vulnerabilities. Do not include any secret values in your output. Open an issue summarizing the findings ranked by severity, with file references and a suggested remediation for each.`, + }, +] + +export function automationTemplateById( + id: string | undefined | null +): AutomationTemplate | undefined { + if (!id) return undefined + return AUTOMATION_TEMPLATES.find((template) => template.id === id) +} diff --git a/ui/src/lib/agents/cron.ts b/ui/src/lib/agents/cron.ts index 36228e79..39d26e91 100644 --- a/ui/src/lib/agents/cron.ts +++ b/ui/src/lib/agents/cron.ts @@ -65,6 +65,11 @@ export function describeCron(expr: string): string { return expr } +/** True when describeCron renders a human-readable label (not the raw expression). */ +export function isDescribableCron(expr: string): boolean { + return describeCron(expr) !== expr +} + /** Match a cron expression back to a known preset id, if any. */ export function presetForCron(expr: string): CronPreset["id"] | "custom" { const normalized = expr.trim().split(/\s+/).join(" ") diff --git a/ui/src/routes/agents/automations/new.tsx b/ui/src/routes/agents/automations/new.tsx index 45c7fc7e..6c850f22 100644 --- a/ui/src/routes/agents/automations/new.tsx +++ b/ui/src/routes/agents/automations/new.tsx @@ -1,11 +1,25 @@ import { createFileRoute } from "@tanstack/react-router" import { AutomationEditor } from "@/components/agents/AutomationEditor" +import { automationTemplateById } from "@/lib/agents/automation-templates" + +interface NewAutomationSearch { + template?: string +} export const Route = createFileRoute("/agents/automations/new")({ + validateSearch: (search: Record): NewAutomationSearch => ({ + template: typeof search.template === "string" ? search.template : undefined, + }), component: NewAutomationPage, }) function NewAutomationPage() { - return + const { template } = Route.useSearch() + return ( + + ) }