Add draft routine defaults and run-time overrides

This commit is contained in:
dotta 2026-04-09 10:19:52 -05:00
parent b4a58ba8a6
commit 5d021583be
18 changed files with 592 additions and 113 deletions

View file

@ -17,7 +17,6 @@ import {
} from "lucide-react";
import { routinesApi, type RoutineTriggerResponse, type RotateRoutineTriggerResponse } from "../api/routines";
import { heartbeatsApi } from "../api/heartbeats";
import { instanceSettingsApi } from "../api/instanceSettings";
import { LiveRunWidget } from "../components/LiveRunWidget";
import { agentsApi } from "../api/agents";
import { projectsApi } from "../api/projects";
@ -35,7 +34,6 @@ import { InlineEntitySelector, type InlineEntityOption } from "../components/Inl
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
import {
RoutineRunVariablesDialog,
routineRunNeedsConfiguration,
type RoutineRunDialogSubmitData,
} from "../components/RoutineRunVariablesDialog";
import { RoutineVariablesEditor, RoutineVariablesHint } from "../components/RoutineVariablesEditor";
@ -123,6 +121,24 @@ function getLocalTimezone(): string {
}
}
function buildRoutineMutationPayload(input: {
title: string;
description: string;
projectId: string;
assigneeAgentId: string;
priority: string;
concurrencyPolicy: string;
catchUpPolicy: string;
variables: RoutineVariable[];
}) {
return {
...input,
description: input.description.trim() || null,
projectId: input.projectId || null,
assigneeAgentId: input.assigneeAgentId || null,
};
}
function TriggerEditor({
trigger,
onSave,
@ -333,11 +349,6 @@ export function RoutineDetail() {
queryFn: () => projectsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const { data: experimentalSettings } = useQuery({
queryKey: queryKeys.instance.experimentalSettings,
queryFn: () => instanceSettingsApi.getExperimental(),
retry: false,
});
const routineDefaults = useMemo(
() =>
@ -345,8 +356,8 @@ export function RoutineDetail() {
? {
title: routine.title,
description: routine.description ?? "",
projectId: routine.projectId,
assigneeAgentId: routine.assigneeAgentId,
projectId: routine.projectId ?? "",
assigneeAgentId: routine.assigneeAgentId ?? "",
priority: routine.priority,
concurrencyPolicy: routine.concurrencyPolicy,
catchUpPolicy: routine.catchUpPolicy,
@ -418,10 +429,7 @@ export function RoutineDetail() {
const saveRoutine = useMutation({
mutationFn: () => {
return routinesApi.update(routineId!, {
...editDraft,
description: editDraft.description.trim() || null,
});
return routinesApi.update(routineId!, buildRoutineMutationPayload(editDraft));
},
onSuccess: async () => {
await Promise.all([
@ -443,6 +451,8 @@ export function RoutineDetail() {
mutationFn: (data?: RoutineRunDialogSubmitData) =>
routinesApi.run(routineId!, {
...(data?.variables && Object.keys(data.variables).length > 0 ? { variables: data.variables } : {}),
...(data?.assigneeAgentId !== undefined ? { assigneeAgentId: data.assigneeAgentId } : {}),
...(data?.projectId !== undefined ? { projectId: data.projectId } : {}),
...(data?.executionWorkspaceId !== undefined ? { executionWorkspaceId: data.executionWorkspaceId } : {}),
...(data?.executionWorkspacePreference !== undefined
? { executionWorkspacePreference: data.executionWorkspacePreference }
@ -657,14 +667,15 @@ export function RoutineDetail() {
}
const automationEnabled = routine.status === "active";
const selectedProject = projects?.find((project) => project.id === routine.projectId) ?? null;
const needsRunConfiguration = routineRunNeedsConfiguration({
variables: routine.variables ?? [],
project: selectedProject,
isolatedWorkspacesEnabled: experimentalSettings?.enableIsolatedWorkspaces === true,
});
const selectedProject = routine.projectId ? (projects?.find((project) => project.id === routine.projectId) ?? null) : null;
const automationToggleDisabled = updateRoutineStatus.isPending || routine.status === "archived";
const automationLabel = routine.status === "archived" ? "Archived" : automationEnabled ? "Active" : "Paused";
const automationLabel = routine.status === "archived"
? "Archived"
: !routine.assigneeAgentId
? "Draft"
: automationEnabled
? "Active"
: "Paused";
const automationLabelClassName = routine.status === "archived"
? "text-muted-foreground"
: automationEnabled
@ -708,18 +719,24 @@ export function RoutineDetail() {
<div className="flex shrink-0 items-center gap-3 pt-1">
<RunButton
onClick={() => {
if (needsRunConfiguration) {
setRunVariablesOpen(true);
return;
}
runRoutine.mutate({});
setRunVariablesOpen(true);
}}
disabled={runRoutine.isPending}
/>
<ToggleSwitch
size="lg"
checked={automationEnabled}
onCheckedChange={() => updateRoutineStatus.mutate(automationEnabled ? "paused" : "active")}
onCheckedChange={() => {
if (!automationEnabled && !routine.assigneeAgentId) {
pushToast({
title: "Default agent required",
body: "Set a default agent before enabling routine automation.",
tone: "warn",
});
return;
}
updateRoutineStatus.mutate(automationEnabled ? "paused" : "active");
}}
disabled={automationToggleDisabled}
aria-label={automationEnabled ? "Pause automatic triggers" : "Enable automatic triggers"}
/>
@ -755,6 +772,12 @@ export function RoutineDetail() {
</div>
)}
{!routine.assigneeAgentId ? (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 text-sm text-amber-900 dark:text-amber-200">
Default agent required. This routine can stay as a draft and still run manually, but automation stays paused until you assign a default agent.
</div>
) : null}
{/* Assignment row */}
<div className="overflow-x-auto overscroll-x-contain">
<div className="inline-flex min-w-full flex-wrap items-center gap-2 text-sm text-muted-foreground sm:min-w-max sm:flex-nowrap">
@ -853,7 +876,7 @@ export function RoutineDetail() {
bordered={false}
contentClassName="min-h-[120px] text-[15px] leading-7"
onSubmit={() => {
if (!saveRoutine.isPending && editDraft.title.trim() && editDraft.projectId && editDraft.assigneeAgentId) {
if (!saveRoutine.isPending && editDraft.title.trim()) {
saveRoutine.mutate();
}
}}
@ -921,7 +944,7 @@ export function RoutineDetail() {
)}
<Button
onClick={() => saveRoutine.mutate()}
disabled={saveRoutine.isPending || !editDraft.title.trim() || !editDraft.projectId || !editDraft.assigneeAgentId}
disabled={saveRoutine.isPending || !editDraft.title.trim()}
>
<Save className="mr-2 h-4 w-4" />
Save routine
@ -1091,7 +1114,10 @@ export function RoutineDetail() {
open={runVariablesOpen}
onOpenChange={setRunVariablesOpen}
companyId={routine.companyId}
project={selectedProject}
agents={agents ?? []}
projects={projects ?? []}
defaultProjectId={routine.projectId}
defaultAssigneeAgentId={routine.assigneeAgentId}
variables={routine.variables ?? []}
isPending={runRoutine.isPending}
onSubmit={(data) => runRoutine.mutate(data)}