mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 14:09:22 +02:00
* 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
131 lines
4.9 KiB
TypeScript
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) });
|
|
},
|
|
});
|
|
}
|