From 247c56c6b0cbebb887efdefe65d1a5ddd309da1d Mon Sep 17 00:00:00 2001
From: Jenn Mueng <30991498+jennmueng@users.noreply.github.com>
Date: Thu, 20 Nov 2025 23:45:23 +0700
Subject: [PATCH 1/5] feat(cursor-agent): add frontend toggle for whether to
create pr
---
static/app/components/events/autofix/types.ts | 1 +
.../views/settings/projectSeer/index.spec.tsx | 202 ++++++++++++++++++
.../app/views/settings/projectSeer/index.tsx | 77 +++++++
3 files changed, 280 insertions(+)
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..5eab804bbf9cdf 100644
--- a/static/app/views/settings/projectSeer/index.spec.tsx
+++ b/static/app/views/settings/projectSeer/index.spec.tsx
@@ -537,6 +537,7 @@ describe('ProjectSeer', () => {
handoff_point: 'root_cause',
target: 'cursor_background_agent',
integration_id: 123,
+ auto_create_pr: false,
},
}),
})
@@ -699,4 +700,205 @@ 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: [],
+ },
+ });
+
+ 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..9a47c80b8cb98b 100644
--- a/static/app/views/settings/projectSeer/index.tsx
+++ b/static/app/views/settings/projectSeer/index.tsx
@@ -100,6 +100,55 @@ const autofixAutomationToggleField = {
}),
} satisfies FieldObject;
+function CodingAgentSettings({
+ preference,
+ handleAutoCreatePrChange,
+ canWriteProject,
+ autofixAutomationTuning,
+}: {
+ autofixAutomationTuning: boolean;
+ canWriteProject: boolean;
+ handleAutoCreatePrChange: (value: boolean) => void;
+ preference: any;
+}) {
+ if (!preference?.automation_handoff || !autofixAutomationTuning) {
+ return null;
+ }
+
+ return (
+
+ );
+}
+
function ProjectSeerGeneralForm({project}: {project: Project}) {
const organization = useOrganization();
const queryClient = useQueryClient();
@@ -146,6 +195,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 +209,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?'),
@@ -291,6 +358,16 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {
)}
/>
+
);
}
From 9c3bf8c8e7393c17432651920822d855f88d627f Mon Sep 17 00:00:00 2001
From: Jenn Mueng <30991498+jennmueng@users.noreply.github.com>
Date: Fri, 21 Nov 2025 01:55:15 +0700
Subject: [PATCH 2/5] type, fields, test
---
.../views/settings/projectSeer/index.spec.tsx | 13 ++++++++
.../app/views/settings/projectSeer/index.tsx | 30 +++++++++++--------
2 files changed, 30 insertions(+), 13 deletions(-)
diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx
index 5eab804bbf9cdf..b5e642f684ac8a 100644
--- a/static/app/views/settings/projectSeer/index.spec.tsx
+++ b/static/app/views/settings/projectSeer/index.spec.tsx
@@ -867,6 +867,19 @@ describe('ProjectSeer', () => {
},
});
+ 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',
diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx
index 9a47c80b8cb98b..d055ced86d82f4 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';
@@ -109,20 +110,21 @@ function CodingAgentSettings({
autofixAutomationTuning: boolean;
canWriteProject: boolean;
handleAutoCreatePrChange: (value: boolean) => void;
- preference: any;
+ preference: ProjectSeerPreferences | null | undefined;
}) {
if (!preference?.automation_handoff || !autofixAutomationTuning) {
return null;
}
+ const initialValue = preference?.automation_handoff?.auto_create_pr ?? false;
+
return (