diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx
index b6a9b053e..58079bda3 100644
--- a/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx
+++ b/apps/web/src/components/settings/automations/AutomationsSettings.render.client.test.tsx
@@ -6,11 +6,13 @@ import {
screen,
waitFor,
} from '@testing-library/react';
+import { toast } from 'sonner';
const managerInstructionsPlaceholder =
/Optional guidance for which ideas to prioritize or avoid/;
const state = vi.hoisted(() => ({
customAutomationsPending: false,
+ customAutomationRunPendingId: null as string | null,
customAutomations: [] as Array<{
id: string;
name: string;
@@ -252,9 +254,13 @@ const mutations = vi.hoisted(() => ({
} | null,
latestTriggerOptions: null as {
onSuccess?: (
- result: NonNullable,
+ result: { outcome: 'launched'; taskId: string },
+ variables: { automationKey: string },
) => void;
} | null,
+ latestCustomTriggerOptions: null as {
+ onSuccess?: (result: { outcome: 'launched'; taskId: string }) => void;
+ } | null,
}));
vi.mock('next/link', () => ({
@@ -276,6 +282,8 @@ vi.mock('sonner', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
+ info: vi.fn(),
+ message: vi.fn(),
},
}));
@@ -364,9 +372,13 @@ vi.mock('@tanstack/react-query', () => ({
) => void;
onError?: (...args: unknown[]) => void;
mutationKind?: 'triggerCustomAutomation';
+ mutationKey?: unknown[];
}) => {
return {
- isPending: false,
+ isPending:
+ _options?.mutationKind === 'triggerCustomAutomation' &&
+ (typeof _options.mutationKey?.[1] !== 'string' ||
+ _options.mutationKey[1] === state.customAutomationRunPendingId),
mutate: vi.fn((variables: unknown) => {
if (_options?.mutationKind === 'triggerCustomAutomation') {
mutations.triggerCustomAutomation(variables);
@@ -421,10 +433,15 @@ vi.mock('@/trpc/client', () => ({
mutationOptions: (options?: Record) => options ?? {},
},
triggerCustomAutomation: {
- mutationOptions: (options?: Record) => ({
- ...options,
- mutationKind: 'triggerCustomAutomation',
- }),
+ mutationOptions: (options?: Record) => {
+ mutations.latestCustomTriggerOptions =
+ (options as typeof mutations.latestCustomTriggerOptions) ?? null;
+
+ return {
+ ...options,
+ mutationKind: 'triggerCustomAutomation',
+ };
+ },
},
resolveCustomAutomationSchedule: {
mutationOptions: (options?: Record) => options ?? {},
@@ -504,8 +521,10 @@ describe('AutomationsSettings', () => {
beforeEach(() => {
vi.clearAllMocks();
state.nextUpdateSettingsResult = null;
+ state.customAutomationRunPendingId = null;
mutations.latestSettingsOptions = null;
mutations.latestTriggerOptions = null;
+ mutations.latestCustomTriggerOptions = null;
state.settingsQuery.data.capabilities.slackConnected = true;
state.settingsQuery.data.capabilities.discordConnected = false;
state.settingsQuery.data.capabilities.telegramConnected = false;
@@ -895,6 +914,24 @@ describe('AutomationsSettings', () => {
).toBeInTheDocument();
});
+ it('names built-in automations in run-now task toasts', () => {
+ render();
+
+ act(() => {
+ mutations.latestTriggerOptions?.onSuccess?.(
+ { outcome: 'launched', taskId: 'task-built-in-1' },
+ { automationKey: 'suggester' },
+ );
+ });
+
+ expect(toast.success).toHaveBeenCalledWith(
+ 'Running Suggest Ideas now',
+ expect.objectContaining({
+ action: expect.objectContaining({ label: 'View task' }),
+ }),
+ );
+ });
+
it('renders custom automations as a compact control list and honors their permalinks', async () => {
state.environments = [{ id: 'env-1', name: 'Production' }];
state.customAutomations = [
@@ -937,6 +974,35 @@ describe('AutomationsSettings', () => {
expect(mutations.triggerCustomAutomation).toHaveBeenCalledWith({
id: 'automation-1',
});
+ act(() => {
+ mutations.latestCustomTriggerOptions?.onSuccess?.({
+ outcome: 'launched',
+ taskId: 'task-custom-1',
+ });
+ });
+ expect(toast.success).toHaveBeenCalledWith(
+ 'Running Weekly flaky-test scan now',
+ expect.objectContaining({
+ action: expect.objectContaining({ label: 'View task' }),
+ }),
+ );
+
+ state.customAutomations.push({
+ ...state.customAutomations[0]!,
+ id: 'automation-2',
+ name: 'Daily dependency scan',
+ });
+ state.customAutomationRunPendingId = 'automation-1';
+ rerender();
+ expect(
+ screen.getByRole('button', { name: 'Run Weekly flaky-test scan now' }),
+ ).toBeDisabled();
+ expect(
+ screen.getByRole('button', { name: 'Run Daily dependency scan now' }),
+ ).toBeEnabled();
+ expect(screen.getByRole('button', { name: 'New' })).toBeEnabled();
+
+ state.customAutomationRunPendingId = null;
state.customAutomations[0]!.enabled = false;
rerender();
expect(
diff --git a/apps/web/src/components/settings/automations/AutomationsSettings.tsx b/apps/web/src/components/settings/automations/AutomationsSettings.tsx
index e529c4e6a..27935608e 100644
--- a/apps/web/src/components/settings/automations/AutomationsSettings.tsx
+++ b/apps/web/src/components/settings/automations/AutomationsSettings.tsx
@@ -1800,9 +1800,9 @@ export function AutomationsSettings() {
switch (data.outcome) {
case 'launched':
- toast.success(`${automationLabel} started a task.`, {
+ toast.success(`Running ${automationLabel} now`, {
action: {
- label: 'Open task',
+ label: 'View task',
onClick: () => window.open(`/task/${data.taskId}`, '_blank'),
},
});
diff --git a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx
index 1d24f7950..8e218f234 100644
--- a/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx
+++ b/apps/web/src/components/settings/automations/CustomAutomationsSection.tsx
@@ -119,6 +119,62 @@ function cadenceLabel(row: CustomAutomationListItem): string {
: 'Custom schedule';
}
+function CustomAutomationRunButton({
+ automation,
+ disabled,
+}: {
+ automation: CustomAutomationListItem;
+ disabled: boolean;
+}) {
+ const trpc = useTRPC();
+ const triggerMutation = useMutation({
+ ...trpc.automations.triggerCustomAutomation.mutationOptions({
+ onSuccess: (result) => {
+ switch (result.outcome) {
+ case 'launched':
+ toast.success(`Running ${automation.name} now`, {
+ action: {
+ label: 'View task',
+ onClick: () => window.open(`/task/${result.taskId}`, '_blank'),
+ },
+ });
+ break;
+ case 'completed':
+ toast.success(`${automation.name} ran successfully.`);
+ break;
+ case 'skipped':
+ toast.info(
+ `${automation.name} had nothing to do: ${result.reason}`,
+ );
+ break;
+ case 'failed':
+ toast.error(`${automation.name} failed: ${result.error}`);
+ break;
+ }
+ },
+ onError: (error) => {
+ toast.error(error.message || `Failed to run ${automation.name}`);
+ },
+ }),
+ mutationKey: ['customAutomationRun', automation.id],
+ });
+
+ return (
+
+
+
+ );
+}
+
function targetFromRow(row: CustomAutomationListItem): {
provider: CustomAutomationFormState['targetProvider'];
channelId: string;
@@ -350,35 +406,6 @@ export function CustomAutomationsSection() {
}),
);
- const triggerMutation = useMutation(
- trpc.automations.triggerCustomAutomation.mutationOptions({
- onSuccess: (result) => {
- switch (result.outcome) {
- case 'launched':
- toast.success('Custom automation started a task.', {
- action: {
- label: 'Open task',
- onClick: () => window.open(`/task/${result.taskId}`, '_blank'),
- },
- });
- break;
- case 'completed':
- toast.success('Custom automation ran successfully.');
- break;
- case 'skipped':
- toast.info(`Custom automation had nothing to do: ${result.reason}`);
- break;
- case 'failed':
- toast.error(`Custom automation failed: ${result.error}`);
- break;
- }
- },
- onError: (error) => {
- toast.error(error.message || 'Failed to run custom automation');
- },
- }),
- );
-
const resolveScheduleMutation = useMutation(
trpc.automations.resolveCustomAutomationSchedule.mutationOptions({
onSuccess: (result, variables) => {
@@ -431,8 +458,7 @@ export function CustomAutomationsSection() {
createMutation.isPending ||
updateMutation.isPending ||
deleteMutation.isPending ||
- toggleMutation.isPending ||
- triggerMutation.isPending;
+ toggleMutation.isPending;
const closeEditor = () => {
setIsCreating(false);
@@ -1062,18 +1088,10 @@ export function CustomAutomationsSection() {
-
-
-
+