feat: add automation template gallery (#1608)

Add a "Start from a template" gallery to the Automations page so users can
scaffold common scheduled agent runs (PR review digest, issue triage,
dependency checks, flaky-test tracking, release notes, docs freshness,
security audit) instead of writing every automation from scratch. Selecting
a template opens the new-automation editor prefilled with its instructions
and a default schedule, which the user can tweak before saving.

Also adds an isDescribableCron helper so template schedules render as
human-readable labels rather than raw cron in the editor.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Johannes du Plessis 2026-06-25 10:36:34 -07:00 • committed by GitHub
parent fb467e84fd
commit f6c215fff7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 178 additions and 7 deletions

View file

@ -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<string | null>(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<string | null>(initialCron)
const [customMode, setCustomMode] = useState(
schedule ? presetForCron(schedule.schedule) === "custom" : false
initialCron ? !isDescribableCron(initialCron) : false
)
const [repo, setRepo] = useState<string | null>(schedule?.repo ?? null)
const [enabled, setEnabled] = useState(schedule?.enabled ?? true)

View file

@ -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 (
<div className="mt-10">
<h2 className="text-xs font-medium text-[var(--ui-text-muted)]">
Start from a template
</h2>
<p className="mt-1 text-xs text-[var(--ui-text-dim)]">
Prefilled instructions and a schedule you can tweak before saving.
</p>
<div className="mt-3 grid gap-3 sm:grid-cols-2">
{AUTOMATION_TEMPLATES.map((template) => {
const Icon = template.icon
return (
<Link
key={template.id}
to="/agents/automations/new"
search={{ template: template.id }}
className="flex flex-col rounded-xl border border-[var(--ui-border)] bg-[var(--ui-surface)] px-4 py-3 transition-colors hover:border-[var(--ui-text-dim)]"
>
<div className="flex items-center gap-2">
<Icon className="size-4 shrink-0 text-[var(--ui-text-muted)]" />
<span className="truncate text-sm font-medium text-[var(--ui-text)]">
{template.name}
</span>
</div>
<p className="mt-1.5 text-xs leading-relaxed text-[var(--ui-text-muted)]">
{template.description}
</p>
<span className="mt-2 flex items-center gap-1 text-xs text-[var(--ui-text-dim)]">
<ClockIcon className="size-3.5 shrink-0" />
{describeCron(template.schedule)}
</span>
</Link>
)
})}
</div>
</div>
)
}

View file

@ -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() {
</div>
)}
</div>
<AutomationTemplates />
</div>
</div>
)

View file

@ -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<AutomationTemplate> = [
{
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)
}

View file

@ -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(" ")

View file

@ -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<string, unknown>): NewAutomationSearch => ({
template: typeof search.template === "string" ? search.template : undefined,
}),
component: NewAutomationPage,
})
function NewAutomationPage() {
return <AutomationEditor mode="create" />
const { template } = Route.useSearch()
return (
<AutomationEditor
mode="create"
template={automationTemplateById(template)}
/>
)
}