Files
multica/packages/core/autopilots/mutations.ts
Jiayuan Zhang d88fe2608e feat(autopilot): scheduled/triggered automations for AI agents (#1028)
* feat(autopilot): add scheduled/triggered automation for AI agents

Introduce the Autopilot feature — recurring automations that assign work
to AI agents on a schedule or manual trigger. Supports two execution
modes: create_issue (creates an issue for the agent to work on) and
run_only (directly enqueues an agent task without issue pollution).

Backend: migration (3 tables + 2 columns), sqlc queries, AutopilotService
with concurrency policies (skip/queue/replace), HTTP CRUD + trigger
endpoints, background cron scheduler (30s tick), event listeners for
issue→run and task→run status sync.

Frontend: types, API client methods, TanStack Query hooks with optimistic
mutations, realtime cache invalidation, list page with create dialog,
detail page with trigger management and run history, sidebar nav + routes
for both web and desktop apps.

* feat(autopilot): improve UX — trigger config, edit dialog, template gallery

- Replace raw cron input with friendly frequency tabs (Hourly/Daily/Weekdays/Weekly/Custom), time picker, and timezone dropdown defaulting to user's local timezone
- Fix Select components showing UUIDs instead of names (Base UI render function pattern)
- Add Edit button on detail page opening a unified edit dialog
- Remove project/concurrency/issue-title-template from create/edit (simplify for users)
- Add trigger configuration inline during autopilot creation
- Add template gallery on empty state (6 step-by-step workflow templates)
- Rename "Description" to "Prompt" throughout UI
- Inject autopilot run timestamp into issue description for agent date awareness
- Treat issue status "in_review" as run completion (fixes skip on next trigger)
- Make migration idempotent with IF NOT EXISTS clauses
2026-04-15 04:54:37 +08:00

131 lines
4.9 KiB
TypeScript

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "../api";
import { autopilotKeys } from "./queries";
import { useWorkspaceId } from "../hooks";
import type {
CreateAutopilotRequest,
UpdateAutopilotRequest,
ListAutopilotsResponse,
GetAutopilotResponse,
CreateAutopilotTriggerRequest,
UpdateAutopilotTriggerRequest,
} from "../types";
export function useCreateAutopilot() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: (data: CreateAutopilotRequest) => api.createAutopilot(data),
onSuccess: (newAutopilot) => {
qc.setQueryData<ListAutopilotsResponse>(autopilotKeys.list(wsId), (old) =>
old && !old.autopilots.some((a) => a.id === newAutopilot.id)
? { ...old, autopilots: [...old.autopilots, newAutopilot], total: old.total + 1 }
: old,
);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: autopilotKeys.list(wsId) });
},
});
}
export function useUpdateAutopilot() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: ({ id, ...data }: { id: string } & UpdateAutopilotRequest) =>
api.updateAutopilot(id, data),
onMutate: ({ id, ...data }) => {
qc.cancelQueries({ queryKey: autopilotKeys.list(wsId) });
const prevList = qc.getQueryData<ListAutopilotsResponse>(autopilotKeys.list(wsId));
const prevDetail = qc.getQueryData<GetAutopilotResponse>(autopilotKeys.detail(wsId, id));
qc.setQueryData<ListAutopilotsResponse>(autopilotKeys.list(wsId), (old) =>
old ? { ...old, autopilots: old.autopilots.map((a) => (a.id === id ? { ...a, ...data } : a)) } : old,
);
qc.setQueryData<GetAutopilotResponse>(autopilotKeys.detail(wsId, id), (old) =>
old ? { ...old, autopilot: { ...old.autopilot, ...data } } : old,
);
return { prevList, prevDetail, id };
},
onError: (_err, _vars, ctx) => {
if (ctx?.prevList) qc.setQueryData(autopilotKeys.list(wsId), ctx.prevList);
if (ctx?.prevDetail) qc.setQueryData(autopilotKeys.detail(wsId, ctx.id), ctx.prevDetail);
},
onSettled: (_data, _err, vars) => {
qc.invalidateQueries({ queryKey: autopilotKeys.detail(wsId, vars.id) });
qc.invalidateQueries({ queryKey: autopilotKeys.list(wsId) });
},
});
}
export function useDeleteAutopilot() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: (id: string) => api.deleteAutopilot(id),
onMutate: async (id) => {
await qc.cancelQueries({ queryKey: autopilotKeys.list(wsId) });
const prevList = qc.getQueryData<ListAutopilotsResponse>(autopilotKeys.list(wsId));
qc.setQueryData<ListAutopilotsResponse>(autopilotKeys.list(wsId), (old) =>
old ? { ...old, autopilots: old.autopilots.filter((a) => a.id !== id), total: old.total - 1 } : old,
);
qc.removeQueries({ queryKey: autopilotKeys.detail(wsId, id) });
return { prevList };
},
onError: (_err, _id, ctx) => {
if (ctx?.prevList) qc.setQueryData(autopilotKeys.list(wsId), ctx.prevList);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: autopilotKeys.list(wsId) });
},
});
}
export function useTriggerAutopilot() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: (id: string) => api.triggerAutopilot(id),
onSettled: (_data, _err, id) => {
qc.invalidateQueries({ queryKey: autopilotKeys.runs(wsId, id) });
qc.invalidateQueries({ queryKey: autopilotKeys.detail(wsId, id) });
},
});
}
export function useCreateAutopilotTrigger() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: ({ autopilotId, ...data }: { autopilotId: string } & CreateAutopilotTriggerRequest) =>
api.createAutopilotTrigger(autopilotId, data),
onSettled: (_data, _err, vars) => {
qc.invalidateQueries({ queryKey: autopilotKeys.detail(wsId, vars.autopilotId) });
},
});
}
export function useUpdateAutopilotTrigger() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: ({ autopilotId, triggerId, ...data }: { autopilotId: string; triggerId: string } & UpdateAutopilotTriggerRequest) =>
api.updateAutopilotTrigger(autopilotId, triggerId, data),
onSettled: (_data, _err, vars) => {
qc.invalidateQueries({ queryKey: autopilotKeys.detail(wsId, vars.autopilotId) });
},
});
}
export function useDeleteAutopilotTrigger() {
const qc = useQueryClient();
const wsId = useWorkspaceId();
return useMutation({
mutationFn: ({ autopilotId, triggerId }: { autopilotId: string; triggerId: string }) =>
api.deleteAutopilotTrigger(autopilotId, triggerId),
onSettled: (_data, _err, vars) => {
qc.invalidateQueries({ queryKey: autopilotKeys.detail(wsId, vars.autopilotId) });
},
});
}