diff --git a/static/app/components/events/autofix/types.ts b/static/app/components/events/autofix/types.ts
index 5629e642dd33b0..734bf51848b95f 100644
--- a/static/app/components/events/autofix/types.ts
+++ b/static/app/components/events/autofix/types.ts
@@ -315,6 +315,7 @@ interface SeerAutomationHandoffConfiguration {
handoff_point: 'root_cause';
integration_id: number;
target: 'cursor_background_agent';
+ auto_create_pr?: boolean;
}
export interface ProjectSeerPreferences {
diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx
index 24eaeb2090b6cc..cd2efb3a047b73 100644
--- a/static/app/views/settings/projectSeer/index.spec.tsx
+++ b/static/app/views/settings/projectSeer/index.spec.tsx
@@ -304,21 +304,12 @@ describe('ProjectSeer', () => {
// Open the menu and select a new value
await userEvent.click(select);
- const options = await screen.findAllByText('Minimally Actionable and Above');
+ const options = await screen.findAllByText('Highly Actionable and Above');
expect(options[0]).toBeDefined();
if (options[0]) {
await userEvent.click(options[0]);
}
- // Reopen the menu to select another value
- await userEvent.click(select);
-
- const options2 = await screen.findAllByText('Highly Actionable and Above');
- expect(options2[0]).toBeDefined();
- if (options2[0]) {
- await userEvent.click(options2[0]);
- }
-
// Form has saveOnBlur=true, so wait for the PUT request
await waitFor(() => {
expect(projectPutRequest).toHaveBeenCalledTimes(1);
@@ -537,6 +528,7 @@ describe('ProjectSeer', () => {
handoff_point: 'root_cause',
target: 'cursor_background_agent',
integration_id: 123,
+ auto_create_pr: false,
},
}),
})
@@ -699,4 +691,218 @@ describe('ProjectSeer', () => {
});
});
});
+
+ describe('Auto Create PR Setting', () => {
+ it('does not render when stopping point is not cursor_handoff', async () => {
+ const initialProject: Project = {
+ ...project,
+ autofixAutomationTuning: 'medium',
+ seerScannerAutomation: true,
+ };
+
+ render(, {
+ organization,
+ outletContext: {project: initialProject},
+ });
+
+ // Wait for the page to load
+ await screen.findByText(/Automation/i);
+
+ // The toggle should NOT be visible when stopping point is not cursor_handoff
+ expect(
+ screen.queryByRole('checkbox', {
+ name: /Auto-Create Pull Requests/i,
+ })
+ ).not.toBeInTheDocument();
+ });
+
+ it('renders and loads initial value when cursor_handoff is selected', async () => {
+ MockApiClient.clearMockResponses();
+
+ const orgWithCursorFeature = OrganizationFixture({
+ features: ['autofix-seer-preferences', 'integrations-cursor'],
+ });
+
+ const initialProject: Project = {
+ ...project,
+ autofixAutomationTuning: 'medium',
+ seerScannerAutomation: true,
+ };
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
+ method: 'GET',
+ body: {
+ setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
+ billing: {hasAutofixQuota: true, hasScannerQuota: true},
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/repos/`,
+ query: {status: 'active'},
+ method: 'GET',
+ body: [],
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/integrations/coding-agents/`,
+ method: 'GET',
+ body: {
+ integrations: [
+ {
+ id: '123',
+ name: 'Cursor',
+ provider: 'cursor',
+ },
+ ],
+ },
+ });
+
+ // Mock preferences with automation_handoff including auto_create_pr
+ MockApiClient.addMockResponse({
+ url: `/projects/${orgWithCursorFeature.slug}/${project.slug}/seer/preferences/`,
+ method: 'GET',
+ body: {
+ preference: {
+ organization_id: orgWithCursorFeature.id,
+ project_id: project.id,
+ repositories: [],
+ automated_run_stopping_point: 'root_cause',
+ automation_handoff: {
+ handoff_point: 'root_cause',
+ target: 'cursor_background_agent',
+ integration_id: 123,
+ auto_create_pr: true,
+ },
+ },
+ code_mapping_repos: [],
+ },
+ });
+
+ render(, {
+ organization: orgWithCursorFeature,
+ outletContext: {project: initialProject},
+ });
+
+ // Wait for the toggle to load
+ const toggle = await screen.findByRole('checkbox', {
+ name: /Auto-Create Pull Requests/i,
+ });
+
+ // Verify it's checked
+ await waitFor(() => {
+ expect(toggle).toBeChecked();
+ });
+ });
+
+ it('calls update mutation when toggled', async () => {
+ MockApiClient.clearMockResponses();
+
+ const orgWithCursorFeature = OrganizationFixture({
+ features: ['autofix-seer-preferences', 'integrations-cursor'],
+ });
+
+ const initialProject: Project = {
+ ...project,
+ autofixAutomationTuning: 'medium',
+ seerScannerAutomation: true,
+ };
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/seer/setup-check/`,
+ method: 'GET',
+ body: {
+ setupAcknowledgement: {orgHasAcknowledged: true, userHasAcknowledged: true},
+ billing: {hasAutofixQuota: true, hasScannerQuota: true},
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/repos/`,
+ query: {status: 'active'},
+ method: 'GET',
+ body: [],
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${orgWithCursorFeature.slug}/integrations/coding-agents/`,
+ method: 'GET',
+ body: {
+ integrations: [
+ {
+ id: '123',
+ name: 'Cursor',
+ provider: 'cursor',
+ },
+ ],
+ },
+ });
+
+ // Mock preferences with automation_handoff
+ MockApiClient.addMockResponse({
+ url: `/projects/${orgWithCursorFeature.slug}/${project.slug}/seer/preferences/`,
+ method: 'GET',
+ body: {
+ preference: {
+ repositories: [],
+ automated_run_stopping_point: 'root_cause',
+ automation_handoff: {
+ handoff_point: 'root_cause',
+ target: 'cursor_background_agent',
+ integration_id: 123,
+ auto_create_pr: false,
+ },
+ },
+ code_mapping_repos: [],
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/projects/${orgWithCursorFeature.slug}/${project.slug}/`,
+ method: 'PUT',
+ body: {},
+ });
+
+ // Mock for the Form's empty apiEndpoint POST
+ MockApiClient.addMockResponse({
+ url: '',
+ method: 'POST',
+ body: {},
+ });
+
+ const seerPreferencesPostRequest = MockApiClient.addMockResponse({
+ url: `/projects/${orgWithCursorFeature.slug}/${project.slug}/seer/preferences/`,
+ method: 'POST',
+ });
+
+ render(, {
+ organization: orgWithCursorFeature,
+ outletContext: {project: initialProject},
+ });
+
+ // Find and click the toggle
+ const toggle = await screen.findByRole('checkbox', {
+ name: /Auto-Create Pull Requests/i,
+ });
+ expect(toggle).not.toBeChecked();
+
+ await userEvent.click(toggle);
+
+ // Wait for the POST request to be called
+ await waitFor(() => {
+ expect(seerPreferencesPostRequest).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({
+ data: expect.objectContaining({
+ automation_handoff: expect.objectContaining({
+ auto_create_pr: true,
+ }),
+ repositories: expect.any(Array),
+ }),
+ })
+ );
+ });
+ });
+ });
});
diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx
index 97731e9361f983..40221ea3488fbb 100644
--- a/static/app/views/settings/projectSeer/index.tsx
+++ b/static/app/views/settings/projectSeer/index.tsx
@@ -9,6 +9,7 @@ import {Link} from 'sentry/components/core/link';
import {CursorIntegrationCta} from 'sentry/components/events/autofix/cursorIntegrationCta';
import {useProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useProjectSeerPreferences';
import {useUpdateProjectSeerPreferences} from 'sentry/components/events/autofix/preferences/hooks/useUpdateProjectSeerPreferences';
+import type {ProjectSeerPreferences} from 'sentry/components/events/autofix/types';
import {useCodingAgentIntegrations} from 'sentry/components/events/autofix/useAutofix';
import {useOrganizationSeerSetup} from 'sentry/components/events/autofix/useOrganizationSeerSetup';
import Form from 'sentry/components/forms/form';
@@ -100,6 +101,58 @@ const autofixAutomationToggleField = {
}),
} satisfies FieldObject;
+function CodingAgentSettings({
+ preference,
+ handleAutoCreatePrChange,
+ canWriteProject,
+ isAutomationOn,
+}: {
+ canWriteProject: boolean;
+ handleAutoCreatePrChange: (value: boolean) => void;
+ preference: ProjectSeerPreferences | null | undefined;
+ isAutomationOn?: boolean;
+}) {
+ if (!preference?.automation_handoff || !isAutomationOn) {
+ return null;
+ }
+
+ const initialValue = preference?.automation_handoff?.auto_create_pr ?? false;
+
+ return (
+
+ );
+}
+
function ProjectSeerGeneralForm({project}: {project: Project}) {
const organization = useOrganization();
const queryClient = useQueryClient();
@@ -146,6 +199,7 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
handoff_point: 'root_cause',
target: 'cursor_background_agent',
integration_id: parseInt(cursorIntegration.id, 10),
+ auto_create_pr: false,
},
});
} else {
@@ -159,6 +213,23 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
[updateProjectSeerPreferences, preference?.repositories, cursorIntegration]
);
+ const handleAutoCreatePrChange = useCallback(
+ (value: boolean) => {
+ if (!preference?.automation_handoff) {
+ return;
+ }
+ updateProjectSeerPreferences({
+ repositories: preference?.repositories || [],
+ automated_run_stopping_point: preference?.automated_run_stopping_point,
+ automation_handoff: {
+ ...preference.automation_handoff,
+ auto_create_pr: value,
+ },
+ });
+ },
+ [preference, updateProjectSeerPreferences]
+ );
+
const automatedRunStoppingPointField = {
name: 'automated_run_stopping_point',
label: t('Where should Seer stop?'),
@@ -256,6 +327,10 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
},
];
+ const automationTuning = isTriageSignalsFeatureOn
+ ? (project.autofixAutomationTuning ?? 'off') !== 'off'
+ : (project.autofixAutomationTuning ?? 'off');
+
return (
+
);
}