From 59c351a4e089c7ba9c4b60a3cf38135066e2b446 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Mon, 6 Apr 2026 13:40:53 -0700 Subject: [PATCH 01/17] feat(autofix): Register autofix-retry-from-step feature flag Add a new Flagpole feature flag to gate "Retry from step" buttons on Autofix v3 cards. This is primarily useful for local Seer testing, allowing developers to re-run individual autofix steps without restarting the entire flow. Co-Authored-By: Claude Opus 4.6 --- src/sentry/features/temporary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index b67a8ad03124cd..cf126354c0c63b 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -318,6 +318,8 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:autofix-on-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Autofix to use Seer Explorer V2 designs manager.add("organizations:autofix-on-explorer-v2", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) + # Enable "Retry from step" buttons on Autofix v3 cards (for local Seer testing) + manager.add("organizations:autofix-retry-from-step", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Workflows in Slack manager.add("organizations:seer-slack-workflows", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable new compact issue alert UI in Slack From 3c52843183aa9fcd8e41fed80dd030e49d7a5932 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Mon, 6 Apr 2026 15:02:27 -0700 Subject: [PATCH 02/17] feat(autofix): Thread insert_index through explorer API for retry Pass insert_index from the API serializer through trigger_autofix_explorer to client.continue_run. When provided, Seer truncates blocks after that index, enabling retry-from-step to properly reset downstream steps. Co-Authored-By: Claude Opus 4.6 --- src/sentry/seer/autofix/autofix_agent.py | 2 ++ src/sentry/seer/endpoints/group_ai_autofix.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/sentry/seer/autofix/autofix_agent.py b/src/sentry/seer/autofix/autofix_agent.py index 2e0898cc375f7c..0901b60a14b85f 100644 --- a/src/sentry/seer/autofix/autofix_agent.py +++ b/src/sentry/seer/autofix/autofix_agent.py @@ -200,6 +200,7 @@ def trigger_autofix_explorer( stopping_point: AutofixStoppingPoint | None = None, intelligence_level: Literal["low", "medium", "high"] = "low", user_context: str | None = None, + insert_index: int | None = None, ) -> int: """ Start or continue an Explorer-based autofix run. @@ -247,6 +248,7 @@ def trigger_autofix_explorer( prompt_metadata=prompt_metadata, artifact_key=artifact_key, artifact_schema=artifact_schema, + insert_index=insert_index, ) payload = { diff --git a/src/sentry/seer/endpoints/group_ai_autofix.py b/src/sentry/seer/endpoints/group_ai_autofix.py index d2ccb4f613c8d5..a9c3bc6fa20bf8 100644 --- a/src/sentry/seer/endpoints/group_ai_autofix.py +++ b/src/sentry/seer/endpoints/group_ai_autofix.py @@ -129,6 +129,10 @@ class ExplorerAutofixRequestSerializer(CamelSnakeSerializer): required=False, help_text="Optional repository name for which to create the pull request. Do not pass a repository name to create pull requests in all relevant repositories.", ) + insert_index = serializers.IntegerField( + required=False, + help_text="Block index to insert at. When provided, truncates blocks after this point for retry-from-step.", + ) def validate(self, data: dict[str, Any]) -> dict[str, Any]: stopping_point = data.get("stopping_point", None) @@ -289,6 +293,7 @@ def _post_explorer(self, request: Request, group: Group) -> Response: run_id=run_id, intelligence_level=data["intelligence_level"], user_context=data.get("user_context"), + insert_index=data.get("insert_index"), ) return Response({"run_id": run_id}, status=status.HTTP_202_ACCEPTED) except SeerPermissionError as e: From f40e2733cb1e0b6db90eb45ee40852bd21478fc4 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Mon, 6 Apr 2026 15:22:26 -0700 Subject: [PATCH 03/17] ref(autofix): Remove unused autofix-retry-from-step feature flag The retry buttons are no longer gated behind a feature flag, so this registration is unnecessary. Co-Authored-By: Claude Opus 4.6 --- src/sentry/features/temporary.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sentry/features/temporary.py b/src/sentry/features/temporary.py index cf126354c0c63b..b67a8ad03124cd 100644 --- a/src/sentry/features/temporary.py +++ b/src/sentry/features/temporary.py @@ -318,8 +318,6 @@ def register_temporary_features(manager: FeatureManager) -> None: manager.add("organizations:autofix-on-explorer", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Autofix to use Seer Explorer V2 designs manager.add("organizations:autofix-on-explorer-v2", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) - # Enable "Retry from step" buttons on Autofix v3 cards (for local Seer testing) - manager.add("organizations:autofix-retry-from-step", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable Seer Workflows in Slack manager.add("organizations:seer-slack-workflows", OrganizationFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=True) # Enable new compact issue alert UI in Slack From de0bc7805c7c8e0ff6e7cf036c28f47555b8b529 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 10:51:59 -0700 Subject: [PATCH 04/17] fix(test): Add insert_index to trigger_autofix_explorer mock assertion The test_stopping_point test checks exact kwargs passed to trigger_autofix_explorer. Add the new insert_index=None parameter to match the updated function signature. Co-Authored-By: Claude Opus 4.6 --- tests/sentry/seer/endpoints/test_group_ai_autofix.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index e892e451bf051e..647f9669f5dd34 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -940,6 +940,7 @@ def test_stopping_point(self, mock_trigger_explorer): run_id=None, intelligence_level="low", user_context=None, + insert_index=None, ) @patch("sentry.seer.autofix.autofix._call_autofix") From f299f7af945aa9c6b6d1edc497a389a6d08f6fe2 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 15:21:16 -0700 Subject: [PATCH 05/17] test(autofix): Add test for insert_index pass-through in explorer API Verify that insert_index from the POST request is threaded through to trigger_autofix_explorer for retry-from-step functionality. Co-Authored-By: Claude Opus 4.6 --- .../seer/endpoints/test_group_ai_autofix.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/sentry/seer/endpoints/test_group_ai_autofix.py b/tests/sentry/seer/endpoints/test_group_ai_autofix.py index 647f9669f5dd34..9df8e7a2251649 100644 --- a/tests/sentry/seer/endpoints/test_group_ai_autofix.py +++ b/tests/sentry/seer/endpoints/test_group_ai_autofix.py @@ -943,6 +943,34 @@ def test_stopping_point(self, mock_trigger_explorer): insert_index=None, ) + @patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_explorer") + def test_insert_index_passed_through(self, mock_trigger_explorer): + """POST passes insert_index to trigger_autofix_explorer for retry-from-step.""" + for flag in EXPLORER_FLAGS: + mock_trigger_explorer.reset_mock() + group = self.create_group() + mock_trigger_explorer.return_value = 123 + + self.login_as(user=self.user) + with self.feature(flag): + response = self.client.post( + self._get_url(group.id, mode="explorer"), + data={"step": "solution", "run_id": 42, "insert_index": 3}, + format="json", + ) + + assert response.status_code == 202, f"Failed for {flag}: {response.data}" + mock_trigger_explorer.assert_called_once_with( + group=group, + step=AutofixStep.SOLUTION, + referrer=AutofixReferrer.GROUP_AUTOFIX_ENDPOINT, + stopping_point=None, + run_id=42, + intelligence_level="low", + user_context=None, + insert_index=3, + ) + @patch("sentry.seer.autofix.autofix._call_autofix") @patch("sentry.seer.autofix.autofix._get_trace_tree_for_event") @patch("sentry.tasks.seer.autofix.check_autofix_status.apply_async") From 6dcf2a7efd23e7937f4200e7a445cbc9553f8000 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Mon, 6 Apr 2026 13:42:25 -0700 Subject: [PATCH 06/17] feat(autofix): Add retry buttons to Autofix v3 step cards Add a "Retry" icon button to completed step cards (Plan, Code Changes, Pull Requests) in the v3 Explorer drawer. Clicking the button re-runs that step on the existing run without requiring feedback text. Gated behind the `autofix-retry-from-step` feature flag so it only appears for local Seer testing. RootCauseCard is excluded since the header's "Start Over" button already covers that case. Co-Authored-By: Claude Opus 4.6 --- .../events/autofix/v3/autofixCards.spec.tsx | 191 ++++++++++++++++++ .../events/autofix/v3/autofixCards.tsx | 80 +++++++- 2 files changed, 262 insertions(+), 9 deletions(-) diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index 7cf3e2e5c4bc8f..481f0c949db88c 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -1,3 +1,5 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import {CodingAgentProvider} from 'sentry/components/events/autofix/types'; @@ -618,3 +620,192 @@ describe('CodingAgentCard', () => { expect(screen.getByText('running')).toBeInTheDocument(); }); }); + +describe('Retry button', () => { + const autofixWithRun = { + ...mockAutofix, + runState: {run_id: 42} as any, + isPolling: false, + }; + + it('does not show retry button on SolutionCard without feature flag', () => { + const artifact = makeSolutionArtifact({ + one_line_summary: 'Fix the bug', + steps: [{title: 'Step 1', description: 'Do something'}], + }); + + render( + + ); + + expect(screen.queryByRole('button', {name: 'Retry'})).not.toBeInTheDocument(); + }); + + it('shows retry button on SolutionCard with feature flag', () => { + const artifact = makeSolutionArtifact({ + one_line_summary: 'Fix the bug', + steps: [{title: 'Step 1', description: 'Do something'}], + }); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument(); + }); + + it('calls startStep with solution and runId on SolutionCard retry click', async () => { + const startStep = jest.fn(); + const artifact = makeSolutionArtifact({ + one_line_summary: 'Fix the bug', + steps: [{title: 'Step 1', description: 'Do something'}], + }); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + await userEvent.click(screen.getByRole('button', {name: 'Retry'})); + expect(startStep).toHaveBeenCalledWith('solution', 42); + }); + + it('calls startStep with code_changes and runId on CodeChangesCard retry click', async () => { + const startStep = jest.fn(); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + await userEvent.click(screen.getByRole('button', {name: 'Retry'})); + expect(startStep).toHaveBeenCalledWith('code_changes', 42); + }); + + it('calls createPR with runId on PullRequestsCard retry click', async () => { + const createPR = jest.fn(); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + await userEvent.click(screen.getByRole('button', {name: 'Retry'})); + expect(createPR).toHaveBeenCalledWith(42); + }); + + it('does not show retry button when section is processing', () => { + const artifact = makeSolutionArtifact({ + one_line_summary: 'Fix the bug', + steps: [{title: 'Step 1', description: 'Do something'}], + }); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + expect(screen.queryByRole('button', {name: 'Retry'})).not.toBeInTheDocument(); + }); + + it('does not show retry button on SolutionCard when artifact data is null', () => { + const artifact = makeSolutionArtifact(null); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + expect(screen.queryByRole('button', {name: 'Retry'})).not.toBeInTheDocument(); + }); + + it('disables retry button when isPolling is true', () => { + const artifact = makeSolutionArtifact({ + one_line_summary: 'Fix the bug', + steps: [{title: 'Step 1', description: 'Do something'}], + }); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + expect(screen.getByRole('button', {name: 'Retry'})).toBeDisabled(); + }); + + it('does not show retry button on RootCauseCard even with feature flag', () => { + const artifact = makeRootCauseArtifact({ + one_line_description: 'Null pointer', + five_whys: ['why1'], + }); + + render( + , + { + organization: OrganizationFixture({ + features: ['autofix-retry-from-step'], + }), + } + ); + + expect(screen.queryByRole('button', {name: 'Retry'})).not.toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index a9fc9f309dfbcc..83d1f5354150ef 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -34,6 +34,7 @@ import {IconOpen} from 'sentry/icons/iconOpen'; import {IconPullRequest} from 'sentry/icons/iconPullRequest'; import {t, tct, tn} from 'sentry/locale'; import {defined} from 'sentry/utils'; +import {useOrganization} from 'sentry/utils/useOrganization'; import {FileDiffViewer} from 'sentry/views/seerExplorer/fileDiffViewer'; interface AutofixCardProps { @@ -114,11 +115,22 @@ export function SolutionCard({autofix, section}: AutofixCardProps) { return isSolutionArtifact(sectionArtifact) ? sectionArtifact : null; }, [section]); - const {runState, startStep} = autofix; + const {runState, startStep, isPolling} = autofix; const runId = runState?.run_id; return ( - } title={t('Plan')}> + } + title={t('Plan')} + trailingItems={ + section.status === 'completed' && artifact?.data ? ( + startStep('solution', runId)} + disabled={isPolling} + /> + ) : undefined + } + > {section.status === 'processing' ? ( } title={t('Code Changes')}> + } + title={t('Code Changes')} + trailingItems={ + section.status === 'completed' && patchesByRepo.size > 0 ? ( + startStep('code_changes', runId)} + disabled={isPolling} + /> + ) : undefined + } + > {section.status === 'processing' ? ( { const sectionArtifact = getAutofixArtifactFromSection(section); return isPullRequestsArtifact(sectionArtifact) ? sectionArtifact : null; }, [section]); + const {createPR, runState, isPolling} = autofix; + const runId = runState?.run_id; + return ( - } title={t('Pull Requests')}> + } + title={t('Pull Requests')} + trailingItems={ + section.status === 'completed' && runId ? ( + createPR(runId)} disabled={isPolling} /> + ) : undefined + } + > {artifact?.map(pullRequest => { if (pullRequest.pr_creation_status === 'creating') { return ( @@ -363,17 +397,45 @@ export function CodingAgentCard({section}: AutofixCardProps) { ); } -interface ArtifactCardProps { +interface RetryButtonProps { + disabled: boolean; + onClick: () => void; +} + +function RetryButton({onClick, disabled}: RetryButtonProps) { + const organization = useOrganization(); + + if (!organization.features.includes('autofix-retry-from-step')) { + return null; + } + + return ( + + ); + } return ( !isPullRequestsSection(s) && !isCodingAgentsSection(s)); const referrer = autofix.runState?.blocks?.[0]?.message?.metadata?.referrer; if (!defined(runId) || !defined(section)) { @@ -204,6 +210,11 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte const organization = useOrganization(); const {isPolling, createPR, startStep} = autofix; + const hasPR = useMemo(() => { + const prStates = autofix.runState?.repo_pr_states ?? {}; + return Object.values(prStates).some(pr => pr.pr_number && pr.pr_url); + }, [autofix.runState?.repo_pr_states]); + const handleYesClick = useCallback(() => { createPR(runId); trackAnalytics('autofix.create_pr_clicked', { @@ -233,17 +244,22 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte return null; } + const yesLabel = hasPR ? t('Yes, update the PR') : t('Yes, draft a PR'); + const nevermindLabel = hasPR + ? t('Nevermind, update the PR') + : t('Nevermind, draft a PR'); + return ( ); From 14073d293f5835a9dfefe2e4dce7029463170f5a Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 10:03:03 -0700 Subject: [PATCH 11/17] fix(autofix): Only show PR section when PRs are in sync with code The PR card now only appears when all PRs match the current code changes (commit SHA comparison). This prevents the PR card from showing above the "Are you happy with these code changes?" prompt. - Added areAllPRsInSync() helper for commit SHA comparison - PR section hidden when code is out of sync or no PR exists yet - CodeChangesNextStep returns null when all PRs are in sync Co-Authored-By: Claude Opus 4.6 --- .../events/autofix/useExplorerAutofix.tsx | 46 +++++++++++++++---- .../components/events/autofix/v3/nextStep.tsx | 13 +++++- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 5b02c1151f9295..0db56d4a472a93 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -371,26 +371,22 @@ export function getOrderedAutofixSections(runState: ExplorerAutofixState | null) // Finalize the last in-progress section. finalizeSection(); - // Only show synthetic PR and coding-agent sections when code_changes is - // completed. After a retry-from-step truncates blocks, these would be stale - // and would prevent NextStep buttons from appearing. const hasCompletedCodeChanges = sections.some( s => s.step === 'code_changes' && s.status === 'completed' ); if (hasCompletedCodeChanges) { - const pullRequests = Object.values(runState?.repo_pr_states ?? {}); - if (pullRequests.length) { + // Only show the PR section when all PRs are in sync with current code. + // Otherwise the NextStep prompt handles create/update flow. + const repoPRStates = runState?.repo_pr_states ?? {}; + if (areAllPRsInSync(blocks, repoPRStates)) { + const pullRequests = Object.values(repoPRStates); sections.push({ step: 'pull_request', blockIndex: blocks.length, artifacts: [pullRequests], messages: [], - status: pullRequests.some( - pullRequest => pullRequest.pr_creation_status === 'creating' - ) - ? 'processing' - : 'completed', + status: 'completed', }); } @@ -435,6 +431,36 @@ export function isCodingAgentsSection(section: AutofixSection): boolean { return section.step === 'coding_agents'; } +/** + * Checks if all PRs are in sync with the current code changes. + * A PR is in sync when its commit_sha matches the pr_commit_shas on the + * last block that has merged patches for that repo. + */ +export function areAllPRsInSync( + blocks: Block[], + repoPRStates: Record +): boolean { + const pullRequests = Object.values(repoPRStates); + if (!pullRequests.length) { + return false; + } + return pullRequests.every(pr => { + if (pr.pr_creation_status === 'creating') { + return false; + } + if (!pr.pr_number || !pr.pr_url || !pr.commit_sha) { + return false; + } + for (let i = blocks.length - 1; i >= 0; i--) { + const block = blocks[i]; + if (block?.merged_file_patches?.some(p => p.repo_name === pr.repo_name)) { + return block.pr_commit_shas?.[pr.repo_name] === pr.commit_sha; + } + } + return false; + }); +} + export type AutofixArtifact = | Artifact | ExplorerFilePatch[] diff --git a/static/app/components/events/autofix/v3/nextStep.tsx b/static/app/components/events/autofix/v3/nextStep.tsx index 0fad6c6a8dc9ea..a22134428fe818 100644 --- a/static/app/components/events/autofix/v3/nextStep.tsx +++ b/static/app/components/events/autofix/v3/nextStep.tsx @@ -13,6 +13,7 @@ import { type CodingAgentIntegration, } from 'sentry/components/events/autofix/useAutofix'; import { + areAllPRsInSync, getAutofixArtifactFromSection, isCodeChangesSection, isCodingAgentsSection, @@ -240,7 +241,17 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte const artifact = useMemo(() => getAutofixArtifactFromSection(section), [section]); - if (!defined(artifact)) { + // All PRs are already in sync with current code — nothing for the user to do + const allInSync = useMemo( + () => + areAllPRsInSync( + autofix.runState?.blocks ?? [], + autofix.runState?.repo_pr_states ?? {} + ), + [autofix.runState?.blocks, autofix.runState?.repo_pr_states] + ); + + if (!defined(artifact) || allInSync) { return null; } From 106fbf29f933b10d36ba70568b8157ca762a503c Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 10:09:59 -0700 Subject: [PATCH 12/17] fix(autofix): Show creating/updating state when user confirms PR When the user clicks "Yes, draft/update a PR", immediately show a "Creating PR..." or "Updating PR..." message instead of leaving the NextStep prompt visible during the backend roundtrip. Co-Authored-By: Claude Opus 4.6 --- static/app/components/events/autofix/v3/nextStep.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/app/components/events/autofix/v3/nextStep.tsx b/static/app/components/events/autofix/v3/nextStep.tsx index a22134428fe818..9d67140e291e6c 100644 --- a/static/app/components/events/autofix/v3/nextStep.tsx +++ b/static/app/components/events/autofix/v3/nextStep.tsx @@ -210,6 +210,7 @@ function SolutionNextStep({autofix, group, runId, section, referrer}: NextStepPr function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextStepProps) { const organization = useOrganization(); const {isPolling, createPR, startStep} = autofix; + const [creatingPR, setCreatingPR] = useState(false); const hasPR = useMemo(() => { const prStates = autofix.runState?.repo_pr_states ?? {}; @@ -217,6 +218,7 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte }, [autofix.runState?.repo_pr_states]); const handleYesClick = useCallback(() => { + setCreatingPR(true); createPR(runId); trackAnalytics('autofix.create_pr_clicked', { organization, @@ -255,6 +257,14 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte return null; } + if (creatingPR) { + return ( + + {hasPR ? t('Updating PR\u2026') : t('Creating PR\u2026')} + + ); + } + const yesLabel = hasPR ? t('Yes, update the PR') : t('Yes, draft a PR'); const nevermindLabel = hasPR ? t('Nevermind, update the PR') From 0eb10bdc7fd3de838cded580b146a521d3553572 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 10:14:43 -0700 Subject: [PATCH 13/17] fix(autofix): Show PR card with creating/updating state on confirm When user clicks to create/update PR, immediately hide the NextStep prompt and show the PR card with a disabled "Creating/Updating PR" button. The button text distinguishes between new PRs and updates. The PR section now also appears during the 'creating' status, not just when fully in sync. Co-Authored-By: Claude Opus 4.6 --- .../components/events/autofix/useExplorerAutofix.tsx | 12 +++++++----- .../components/events/autofix/v3/autofixCards.tsx | 5 ++++- static/app/components/events/autofix/v3/nextStep.tsx | 10 +--------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 0db56d4a472a93..0e2db3e03dd207 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -376,17 +376,19 @@ export function getOrderedAutofixSections(runState: ExplorerAutofixState | null) ); if (hasCompletedCodeChanges) { - // Only show the PR section when all PRs are in sync with current code. - // Otherwise the NextStep prompt handles create/update flow. + // Show the PR section when PRs are in sync (View links) or when a PR + // is being created/updated (disabled button with loading text). + // Hide it when PRs are out of sync — the NextStep prompt handles that. const repoPRStates = runState?.repo_pr_states ?? {}; - if (areAllPRsInSync(blocks, repoPRStates)) { - const pullRequests = Object.values(repoPRStates); + const pullRequests = Object.values(repoPRStates); + const anyCreating = pullRequests.some(pr => pr.pr_creation_status === 'creating'); + if (anyCreating || areAllPRsInSync(blocks, repoPRStates)) { sections.push({ step: 'pull_request', blockIndex: blocks.length, artifacts: [pullRequests], messages: [], - status: 'completed', + status: anyCreating ? 'processing' : 'completed', }); } diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index cc68441f22f096..42ba8166dc53c8 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -329,9 +329,12 @@ export function PullRequestsCard({autofix, section}: AutofixCardProps) { > {artifact?.map(pullRequest => { if (pullRequest.pr_creation_status === 'creating') { + const isUpdating = !!(pullRequest.pr_number && pullRequest.pr_url); return ( ); } diff --git a/static/app/components/events/autofix/v3/nextStep.tsx b/static/app/components/events/autofix/v3/nextStep.tsx index 9d67140e291e6c..5378a7b33d3a42 100644 --- a/static/app/components/events/autofix/v3/nextStep.tsx +++ b/static/app/components/events/autofix/v3/nextStep.tsx @@ -253,18 +253,10 @@ function CodeChangesNextStep({autofix, group, runId, section, referrer}: NextSte [autofix.runState?.blocks, autofix.runState?.repo_pr_states] ); - if (!defined(artifact) || allInSync) { + if (!defined(artifact) || allInSync || creatingPR) { return null; } - if (creatingPR) { - return ( - - {hasPR ? t('Updating PR\u2026') : t('Creating PR\u2026')} - - ); - } - const yesLabel = hasPR ? t('Yes, update the PR') : t('Yes, draft a PR'); const nevermindLabel = hasPR ? t('Nevermind, update the PR') From dbcc16dbd56d536a2b052707cf4b80364160558b Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 10:21:29 -0700 Subject: [PATCH 14/17] fix(autofix): Allow retrying failed PR creation When PR creation fails, show a clickable "Retry PR" button instead of a disabled "Failed to create PR" button. Co-Authored-By: Claude Opus 4.6 --- static/app/components/events/autofix/v3/autofixCards.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index 42ba8166dc53c8..ee8e162b59c588 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -369,8 +369,13 @@ export function PullRequestsCard({autofix, section}: AutofixCardProps) { } return ( - ); })} From c835cea3939999476cc6f1a570ebe42f381b5829 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 11:11:40 -0700 Subject: [PATCH 15/17] fix(test): Add blockIndex to AutofixSection test helpers Add missing blockIndex property to makeSection helpers in test files and pr_commit_shas to the autofixSection PR test to match the new sync-aware PR section logic. Co-Authored-By: Claude Opus 4.6 --- .../app/components/events/autofix/v3/autofixPreviews.spec.tsx | 2 +- static/app/components/events/autofix/v3/nextStep.spec.tsx | 1 + .../issueDetails/streamline/sidebar/autofixSection.spec.tsx | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx index f9a35f3a013077..fdd7d46b8aeef1 100644 --- a/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixPreviews.spec.tsx @@ -26,7 +26,7 @@ function makeSection( artifacts: any[] = [], {status}: {status: 'completed' | 'processing'} = {status: 'completed'} ): AutofixSection { - return {step, artifacts, messages: [], status}; + return {step, blockIndex: 0, artifacts, messages: [], status}; } describe('RootCausePreview', () => { diff --git a/static/app/components/events/autofix/v3/nextStep.spec.tsx b/static/app/components/events/autofix/v3/nextStep.spec.tsx index 5efc1cfecc76f8..e9acde211fc0db 100644 --- a/static/app/components/events/autofix/v3/nextStep.spec.tsx +++ b/static/app/components/events/autofix/v3/nextStep.spec.tsx @@ -91,6 +91,7 @@ function makeSection( ): AutofixSection { return { step, + blockIndex: 0, artifacts: artifacts ?? defaultArtifacts(step), messages: [], status: 'completed', diff --git a/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx b/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx index 7e325d7ac655a9..0933a15f7f79a4 100644 --- a/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx +++ b/static/app/views/issueDetails/streamline/sidebar/autofixSection.spec.tsx @@ -301,6 +301,7 @@ describe('AutofixSection', () => { }, }, ], + pr_commit_shas: {'org/repo': 'abc123'}, }, ], repo_pr_states: { From 9ac0edb1c42e1afc3575694f6a66bb306230af74 Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 14:24:31 -0700 Subject: [PATCH 16/17] ref(autofix): Remove PR card header retry, add inline error retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the no-op refresh button from PullRequestsCard header and the dead isOutOfSync/Update code. The error state now shows "Failed to create PR — Retry" as a clickable button. Closed PRs are handled by the normal rerun flow (Seer reopens them automatically). Co-Authored-By: Claude Opus 4.6 --- .../events/autofix/v3/autofixCards.spec.tsx | 14 ------ .../events/autofix/v3/autofixCards.tsx | 44 ++----------------- 2 files changed, 3 insertions(+), 55 deletions(-) diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index 1a9f4136f4519e..38bced5003c3f2 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -698,20 +698,6 @@ describe('Retry button', () => { expect(startStep).toHaveBeenCalledWith('code_changes', 42, undefined, 5); }); - it('calls createPR with runId on PullRequestsCard retry click', async () => { - const createPR = jest.fn(); - - render( - - ); - - await userEvent.click(screen.getByRole('button', {name: 'Retry'})); - expect(createPR).toHaveBeenCalledWith(42); - }); - it('does not show retry button when section is processing', () => { const artifact = makeSolutionArtifact({ one_line_summary: 'Fix the bug', diff --git a/static/app/components/events/autofix/v3/autofixCards.tsx b/static/app/components/events/autofix/v3/autofixCards.tsx index ee8e162b59c588..c6227f4bb7d238 100644 --- a/static/app/components/events/autofix/v3/autofixCards.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useEffect, useMemo, useRef, type ReactNode} from 'react'; +import {Fragment, useEffect, useMemo, useRef, type ReactNode} from 'react'; import styled from '@emotion/styled'; import {Tag} from '@sentry/scraps/badge'; @@ -299,34 +299,8 @@ export function PullRequestsCard({autofix, section}: AutofixCardProps) { const {createPR, runState, isPolling} = autofix; const runId = runState?.run_id; - // Check if code has changed since each PR was last pushed - const isOutOfSync = useCallback( - (repoName: string, prCommitSha: string | null) => { - const blocks = runState?.blocks ?? []; - if (!prCommitSha) { - return true; - } - for (let i = blocks.length - 1; i >= 0; i--) { - const block = blocks[i]; - if (block?.merged_file_patches?.some(p => p.repo_name === repoName)) { - return block.pr_commit_shas?.[repoName] !== prCommitSha; - } - } - return true; - }, - [runState?.blocks] - ); - return ( - } - title={t('Pull Requests')} - trailingItems={ - section.status === 'completed' && runId ? ( - createPR(runId)} disabled={isPolling} /> - ) : undefined - } - > + } title={t('Pull Requests')}> {artifact?.map(pullRequest => { if (pullRequest.pr_creation_status === 'creating') { const isUpdating = !!(pullRequest.pr_number && pullRequest.pr_url); @@ -344,18 +318,6 @@ export function PullRequestsCard({autofix, section}: AutofixCardProps) { pullRequest.pr_url && pullRequest.pr_number ) { - if (isOutOfSync(pullRequest.repo_name, pullRequest.commit_sha) && runId) { - return ( - - ); - } return ( runId && createPR(runId, pullRequest.repo_name)} disabled={isPolling || !runId} > - {t('Retry PR in %s', pullRequest.repo_name)} + {t('Failed to create PR in %s — Retry', pullRequest.repo_name)} ); })} From 292ccf900707dc733fde58b79aefb683ce5f577e Mon Sep 17 00:00:00 2001 From: isaacwang-sentry Date: Tue, 7 Apr 2026 14:48:01 -0700 Subject: [PATCH 17/17] fix(autofix): Show PR card on creation error for retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show the PR section when any PR has an error status, so the "Failed to create PR — Retry" button is visible. Stale errors after retrying earlier steps are cleared on the Seer side. Co-Authored-By: Claude Opus 4.6 --- static/app/components/events/autofix/useExplorerAutofix.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 0e2db3e03dd207..1316c140527cc3 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -382,7 +382,8 @@ export function getOrderedAutofixSections(runState: ExplorerAutofixState | null) const repoPRStates = runState?.repo_pr_states ?? {}; const pullRequests = Object.values(repoPRStates); const anyCreating = pullRequests.some(pr => pr.pr_creation_status === 'creating'); - if (anyCreating || areAllPRsInSync(blocks, repoPRStates)) { + const anyError = pullRequests.some(pr => pr.pr_creation_status === 'error'); + if (anyCreating || anyError || areAllPRsInSync(blocks, repoPRStates)) { sections.push({ step: 'pull_request', blockIndex: blocks.length,