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 (
({}), + getValue: () => initialValue, disabled: !canWriteProject, onChange: handleAutoCreatePrChange, } satisfies FieldObject, @@ -323,6 +327,12 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { }, ]; + const automationTuning = Boolean( + isTriageSignalsFeatureOn + ? (project.autofixAutomationTuning ?? 'off') !== 'off' + : (project.autofixAutomationTuning ?? 'off') + ); + return ( From b14ef124b12076d6bceba01373a1d4a2eec571b0 Mon Sep 17 00:00:00 2001 From: Jenn Mueng <30991498+jennmueng@users.noreply.github.com> Date: Sat, 22 Nov 2025 00:10:43 +0700 Subject: [PATCH 3/5] fix this test --- static/app/views/settings/projectSeer/index.spec.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/static/app/views/settings/projectSeer/index.spec.tsx b/static/app/views/settings/projectSeer/index.spec.tsx index b5e642f684ac8a..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); From 748692054e751382e9ca407087c2fa005b8f2305 Mon Sep 17 00:00:00 2001 From: Jenn Mueng <30991498+jennmueng@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:19:44 +0700 Subject: [PATCH 4/5] need to update the key to stop spinning state --- static/app/views/settings/projectSeer/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx index d055ced86d82f4..64cfc62494931e 100644 --- a/static/app/views/settings/projectSeer/index.tsx +++ b/static/app/views/settings/projectSeer/index.tsx @@ -120,7 +120,7 @@ function CodingAgentSettings({ return ( Date: Wed, 26 Nov 2025 01:15:46 +0700 Subject: [PATCH 5/5] fix condition --- static/app/views/settings/projectSeer/index.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/static/app/views/settings/projectSeer/index.tsx b/static/app/views/settings/projectSeer/index.tsx index 64cfc62494931e..40221ea3488fbb 100644 --- a/static/app/views/settings/projectSeer/index.tsx +++ b/static/app/views/settings/projectSeer/index.tsx @@ -105,14 +105,14 @@ function CodingAgentSettings({ preference, handleAutoCreatePrChange, canWriteProject, - autofixAutomationTuning, + isAutomationOn, }: { - autofixAutomationTuning: boolean; canWriteProject: boolean; handleAutoCreatePrChange: (value: boolean) => void; preference: ProjectSeerPreferences | null | undefined; + isAutomationOn?: boolean; }) { - if (!preference?.automation_handoff || !autofixAutomationTuning) { + if (!preference?.automation_handoff || !isAutomationOn) { return null; } @@ -327,11 +327,9 @@ function ProjectSeerGeneralForm({project}: {project: Project}) { }, ]; - const automationTuning = Boolean( - isTriageSignalsFeatureOn - ? (project.autofixAutomationTuning ?? 'off') !== 'off' - : (project.autofixAutomationTuning ?? 'off') - ); + const automationTuning = isTriageSignalsFeatureOn + ? (project.autofixAutomationTuning ?? 'off') !== 'off' + : (project.autofixAutomationTuning ?? 'off'); return ( @@ -369,7 +367,7 @@ function ProjectSeerGeneralForm({project}: {project: Project}) {