From 6b853f49bb23c34f305fe1b1dc572fdd50d10810 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 22 May 2026 14:14:53 -0400 Subject: [PATCH 1/3] chore(autofix): Remove old useAutofixData hook Replace all usages of useAutofixData with useExplorerAutofix --- .../events/autofix/autofixRootCause.tsx | 46 ---- .../events/autofix/autofixSolution.tsx | 41 ---- static/app/components/events/autofix/types.ts | 212 ------------------ .../components/events/autofix/useAutofix.tsx | 36 --- .../events/autofix/useExplorerAutofix.tsx | 2 +- .../app/components/events/autofix/utils.tsx | 36 +-- .../streamline/eventNavigation.tsx | 4 +- .../hooks/useCopyIssueDetails.spec.tsx | 89 ++++---- .../streamline/hooks/useCopyIssueDetails.tsx | 37 ++- 9 files changed, 79 insertions(+), 424 deletions(-) delete mode 100644 static/app/components/events/autofix/autofixRootCause.tsx delete mode 100644 static/app/components/events/autofix/autofixSolution.tsx diff --git a/static/app/components/events/autofix/autofixRootCause.tsx b/static/app/components/events/autofix/autofixRootCause.tsx deleted file mode 100644 index 382f0aac706764..00000000000000 --- a/static/app/components/events/autofix/autofixRootCause.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type {AutofixRootCauseData} from 'sentry/components/events/autofix/types'; - -export function formatRootCauseText( - cause: AutofixRootCauseData | undefined, - customRootCause?: string -) { - if (!cause && !customRootCause) { - return ''; - } - - if (customRootCause) { - return `# Root Cause of the Issue\n\n${customRootCause}`; - } - - if (!cause) { - return ''; - } - - const parts: string[] = ['# Root Cause of the Issue']; - - if (cause.description) { - parts.push(cause.description); - } - - if (cause.root_cause_reproduction) { - parts.push( - cause.root_cause_reproduction - .map(event => { - const eventParts = [`### ${event.title}`]; - - if (event.code_snippet_and_analysis) { - eventParts.push(event.code_snippet_and_analysis); - } - - if (event.relevant_code_file) { - eventParts.push(`(See @${event.relevant_code_file.file_path})`); - } - - return eventParts.join('\n'); - }) - .join('\n\n') - ); - } - - return parts.join('\n\n'); -} diff --git a/static/app/components/events/autofix/autofixSolution.tsx b/static/app/components/events/autofix/autofixSolution.tsx deleted file mode 100644 index cc1747e28a2de9..00000000000000 --- a/static/app/components/events/autofix/autofixSolution.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type {AutofixSolutionTimelineEvent} from 'sentry/components/events/autofix/types'; - -export function formatSolutionText( - solution: AutofixSolutionTimelineEvent[], - customSolution?: string -) { - if (!solution && !customSolution) { - return ''; - } - - if (customSolution) { - return `# Solution Plan\n\n${customSolution}`; - } - - if (!solution || solution.length === 0) { - return ''; - } - - const parts = ['# Solution Plan']; - - parts.push( - solution - .filter(event => event.is_active) - .map((event, index) => { - const eventParts = [`### ${index + 1}. ${event.title}`]; - - if (event.code_snippet_and_analysis) { - eventParts.push(event.code_snippet_and_analysis); - } - - if (event.relevant_code_file) { - eventParts.push(`(See @${event.relevant_code_file.file_path})`); - } - - return eventParts.join('\n'); - }) - .join('\n\n') - ); - - return parts.join('\n\n'); -} diff --git a/static/app/components/events/autofix/types.ts b/static/app/components/events/autofix/types.ts index 4837aa5ddfef41..0fad08866b7dcf 100644 --- a/static/app/components/events/autofix/types.ts +++ b/static/app/components/events/autofix/types.ts @@ -1,5 +1,4 @@ import {t} from 'sentry/locale'; -import type {User} from 'sentry/types/user'; import {isArrayOf} from 'sentry/types/utils'; export enum DiffFileType { @@ -30,22 +29,6 @@ function isDiffLineType(value: unknown): value is DiffLineType { ); } -export enum AutofixStepType { - DEFAULT = 'default', - ROOT_CAUSE_ANALYSIS = 'root_cause_analysis', - CHANGES = 'changes', - SOLUTION = 'solution', -} - -export enum AutofixStatus { - COMPLETED = 'COMPLETED', - ERROR = 'ERROR', - PROCESSING = 'PROCESSING', - NEED_MORE_INFORMATION = 'NEED_MORE_INFORMATION', - CANCELLED = 'CANCELLED', - WAITING_FOR_USER_RESPONSE = 'WAITING_FOR_USER_RESPONSE', -} - export enum AutofixStoppingPoint { ROOT_CAUSE = 'root_cause', SOLUTION = 'solution', @@ -53,22 +36,6 @@ export enum AutofixStoppingPoint { OPEN_PR = 'open_pr', } -type AutofixPullRequestDetails = { - pr_number: number; - pr_url: string; -}; - -type AutofixOptions = { - iterative_feedback?: boolean; -}; - -interface CodingAgentResult { - description: string; - pr_url: string | null; - repo_full_name: string; - repo_provider: string; -} - export enum CodingAgentStatus { PENDING = 'pending', RUNNING = 'running', @@ -89,185 +56,6 @@ export function getResultButtonLabel(url: string | null | undefined): string { return t('View Pull Request'); } -interface CodingAgentState { - id: string; - name: string; - provider: CodingAgentProvider; - started_at: string; - status: CodingAgentStatus; - agent_url?: string; - results?: CodingAgentResult[]; -} - -type CodebaseState = { - is_readable: boolean | null; - is_writeable: boolean | null; - repo_external_id: string | null; -}; - -export type AutofixData = { - codebases: Record; - last_triggered_at: string; - request: { - repos: SeerRepoDefinition[]; - options?: { - auto_run_source?: string | null; - }; - }; - run_id: string; - status: AutofixStatus; - actor_ids?: number[]; - codebase_indexing?: { - status: 'COMPLETED'; - }; - coding_agents?: Record; - completed_at?: string | null; - error_message?: string; - options?: AutofixOptions; - steps?: AutofixStep[]; - users?: Record; -}; - -type AutofixProgressItem = { - message: string; - timestamp: string; - type: 'INFO' | 'WARNING' | 'ERROR' | 'NEED_MORE_INFORMATION'; - data?: any; -}; - -type AutofixStep = - | AutofixDefaultStep - | AutofixRootCauseStep - | AutofixSolutionStep - | AutofixChangesStep; - -interface BaseStep { - id: string; - index: number; - progress: AutofixProgressItem[]; - status: AutofixStatus; - title: string; - type: AutofixStepType; - active_comment_thread?: CommentThread | null; - agent_comment_thread?: CommentThread | null; - completedMessage?: string; - key?: string; - output_stream?: string | null; -} - -type CommentThread = { - id: string; - is_completed: boolean; - messages: CommentThreadMessage[]; -}; - -interface CommentThreadMessage { - content: string; - role: 'user' | 'assistant'; - isLoading?: boolean; -} - -type AutofixInsight = { - insight: string; - justification: string; - change_diff?: FilePatch[]; - markdown_snippets?: string; - sources?: InsightSources; - type?: 'insight' | 'file_change'; -}; - -type InsightSources = { - breadcrumbs_used: boolean; - code_used_urls: string[]; - connected_error_ids_used: string[]; - diff_urls: string[]; - http_request_used: boolean; - profile_ids_used: string[]; - stacktrace_used: boolean; - thoughts: string; - trace_event_ids_used: string[]; - event_trace_id?: string; - event_trace_timestamp?: number; -}; - -interface AutofixDefaultStep extends BaseStep { - insights: AutofixInsight[]; - type: AutofixStepType.DEFAULT; -} - -type AutofixRootCauseSelection = - | { - cause_id: string; - } - | {custom_root_cause: string} - | null; - -interface AutofixRootCauseStep extends BaseStep { - causes: AutofixRootCauseData[]; - selection: AutofixRootCauseSelection; - type: AutofixStepType.ROOT_CAUSE_ANALYSIS; - termination_reason?: string; -} - -interface AutofixSolutionStep extends BaseStep { - solution: AutofixSolutionTimelineEvent[]; - solution_selected: boolean; - type: AutofixStepType.SOLUTION; - custom_solution?: string; - description?: string; -} - -type AutofixCodebaseChange = { - description: string; - diff: FilePatch[]; - repo_name: string; - title: string; - branch_name?: string; - diff_str?: string; - pull_request?: AutofixPullRequestDetails; - repo_external_id?: string; - repo_id?: number; // The repo_id is only here for temporary backwards compatibility for LA customers, and we should remove it soon. Use repo_external_id instead. -}; - -interface AutofixChangesStep extends BaseStep { - changes: AutofixCodebaseChange[]; - type: AutofixStepType.CHANGES; - termination_reason?: string; -} - -type AutofixRelevantCodeFile = { - file_path: string; - repo_name: string; -}; - -type AutofixRelevantCodeFileWithUrl = AutofixRelevantCodeFile & { - url?: string; -}; - -type AutofixTimelineEvent = { - code_snippet_and_analysis: string; - relevant_code_file: AutofixRelevantCodeFile; - timeline_item_type: 'internal_code' | 'external_system' | 'human_action'; - title: string; - is_most_important_event?: boolean; -}; - -export type AutofixSolutionTimelineEvent = { - timeline_item_type: 'internal_code' | 'human_instruction'; - title: string; - code_snippet_and_analysis?: string; - is_active?: boolean; - is_most_important_event?: boolean; - relevant_code_file?: AutofixRelevantCodeFileWithUrl; -}; - -export type AutofixRootCauseData = { - id: string; - description?: string; - reproduction_urls?: Array; - root_cause_reproduction?: AutofixTimelineEvent[]; -}; - export type FilePatch = { added: number; hunks: Hunk[]; diff --git a/static/app/components/events/autofix/useAutofix.tsx b/static/app/components/events/autofix/useAutofix.tsx index 023e2553b82d4b..abc969f0682d6a 100644 --- a/static/app/components/events/autofix/useAutofix.tsx +++ b/static/app/components/events/autofix/useAutofix.tsx @@ -1,42 +1,6 @@ -import {useQuery} from '@tanstack/react-query'; - -import {type AutofixData} from 'sentry/components/events/autofix/types'; import type {Organization} from 'sentry/types/organization'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import type {RequestError} from 'sentry/utils/requestError/requestError'; -import {useOrganization} from 'sentry/utils/useOrganization'; - -type AutofixResponse = { - autofix: AutofixData | null; -}; - -function autofixApiOptions(orgSlug: string, groupId: string, isUserWatching = false) { - return apiOptions.as()( - '/organizations/$organizationIdOrSlug/issues/$issueId/autofix/', - { - path: {organizationIdOrSlug: orgSlug, issueId: groupId}, - query: {isUserWatching, mode: 'legacy'}, - staleTime: Infinity, - } - ); -} - -export const useAutofixData = ({ - groupId, - isUserWatching = false, -}: { - groupId: string; - isUserWatching?: boolean; -}) => { - const orgSlug = useOrganization().slug; - - const {data, isPending} = useQuery({ - ...autofixApiOptions(orgSlug, groupId, isUserWatching), - enabled: false, - }); - - return {data: data?.autofix ?? null, isPending}; -}; export type CodingAgentIntegration = { id: string | null; diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index 9befd99609f146..73c94332d7d6fe 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -119,7 +119,7 @@ export function isCodingAgentsArtifact( * State returned from the Explorer autofix endpoint. * This extends the SeerExplorer types with autofix-specific data. */ -interface ExplorerAutofixState { +export interface ExplorerAutofixState { blocks: Block[]; run_id: number; status: 'processing' | 'completed' | 'error' | 'awaiting_user_input'; diff --git a/static/app/components/events/autofix/utils.tsx b/static/app/components/events/autofix/utils.tsx index 9ab6b3654d9217..3cfb7d38912ab8 100644 --- a/static/app/components/events/autofix/utils.tsx +++ b/static/app/components/events/autofix/utils.tsx @@ -1,43 +1,9 @@ import {useCallback, useMemo} from 'react'; -import {formatRootCauseText} from 'sentry/components/events/autofix/autofixRootCause'; -import {formatSolutionText} from 'sentry/components/events/autofix/autofixSolution'; -import { - AUTOFIX_TTL_IN_DAYS, - AutofixStepType, - type AutofixData, -} from 'sentry/components/events/autofix/types'; +import {AUTOFIX_TTL_IN_DAYS} from 'sentry/components/events/autofix/types'; import type {Group} from 'sentry/types/group'; import {useOrganization} from 'sentry/utils/useOrganization'; -export function getRootCauseCopyText(autofixData: AutofixData) { - const rootCause = autofixData.steps?.find( - step => step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS - ); - if (!rootCause) { - return null; - } - - const cause = rootCause.causes.at(0); - - if (!cause) { - return null; - } - - return formatRootCauseText(cause); -} - -export function getSolutionCopyText(autofixData: AutofixData) { - const solution = autofixData.steps?.find( - step => step.type === AutofixStepType.SOLUTION - ); - if (!solution) { - return null; - } - - return formatSolutionText(solution.solution, solution.custom_solution); -} - const BASE_SUPPORTED_PROVIDERS = [ 'github', 'integrations:github', diff --git a/static/app/views/issueDetails/streamline/eventNavigation.tsx b/static/app/views/issueDetails/streamline/eventNavigation.tsx index cd49eb2447cdf1..cbb1dcb5818a78 100644 --- a/static/app/views/issueDetails/streamline/eventNavigation.tsx +++ b/static/app/views/issueDetails/streamline/eventNavigation.tsx @@ -12,7 +12,7 @@ import {CopyAsDropdown} from 'sentry/components/copyAsDropdown'; import {Count} from 'sentry/components/count'; import {DropdownButton} from 'sentry/components/dropdownButton'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; -import {useAutofixData} from 'sentry/components/events/autofix/useAutofix'; +import {useExplorerAutofix} from 'sentry/components/events/autofix/useExplorerAutofix'; import {useGroupSummaryData} from 'sentry/components/group/groupSummary'; import {TourElement} from 'sentry/components/tours/components'; import {IconTelescope} from 'sentry/icons'; @@ -117,7 +117,7 @@ export function IssueEventNavigation({event, group}: IssueEventNavigationProps) // Get data for markdown copy functionality const {data: groupSummaryData} = useGroupSummaryData(group); - const {data: autofixData} = useAutofixData({groupId: group.id}); + const {runState: autofixData} = useExplorerAutofix(group.id); const handleCopyMarkdown = useCallback(() => { const markdownText = issueAndEventToMarkdown( diff --git a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx index 2bd3d13b7a0a16..613e30066ec914 100644 --- a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx +++ b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx @@ -5,12 +5,8 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {renderHook, userEvent} from 'sentry-test/reactTestingLibrary'; import * as indicators from 'sentry/actionCreators/indicator'; -import { - AutofixStatus, - AutofixStepType, - type AutofixData, -} from 'sentry/components/events/autofix/types'; -import * as autofixHooks from 'sentry/components/events/autofix/useAutofix'; +import type {ExplorerAutofixState} from 'sentry/components/events/autofix/useExplorerAutofix'; +import * as explorerAutofixHooks from 'sentry/components/events/autofix/useExplorerAutofix'; import type {GroupSummaryData} from 'sentry/components/group/groupSummary'; import * as groupSummaryHooks from 'sentry/components/group/groupSummary'; import {EntryType} from 'sentry/types/event'; @@ -39,46 +35,50 @@ describe('useCopyIssueDetails', () => { possibleCause: 'Missing parameter', }; - // Create a mock AutofixData with steps that includes root cause and solution steps - const mockAutofixData: AutofixData = { - last_triggered_at: '2023-01-01T00:00:00Z', - request: { - repos: [], - }, - codebases: {}, - run_id: '123', - status: AutofixStatus.COMPLETED, - steps: [ + const mockAutofixData: ExplorerAutofixState = { + run_id: 123, + status: 'completed', + updated_at: '2023-01-01T00:00:00Z', + blocks: [ { - id: 'root-cause-step', - index: 0, - progress: [], - status: AutofixStatus.COMPLETED, - title: 'Root Cause', - type: AutofixStepType.ROOT_CAUSE_ANALYSIS, - causes: [ + id: 'root-cause-block', + message: { + role: 'assistant' as const, + content: 'Found the root cause', + metadata: {step: 'root_cause'}, + }, + timestamp: '2023-01-01T00:00:00Z', + loading: false, + artifacts: [ { - id: 'cause-1', - description: 'Root cause text', + key: 'root_cause', + reason: 'Root cause analysis', + data: { + one_line_description: 'Root cause text', + five_whys: ['Why 1'], + }, }, ], - selection: null, }, { - id: 'solution-step', - index: 1, - progress: [], - status: AutofixStatus.COMPLETED, - title: 'Solution', - type: AutofixStepType.SOLUTION, - solution: [ + id: 'solution-block', + message: { + role: 'assistant' as const, + content: 'Here is the solution', + metadata: {step: 'solution'}, + }, + timestamp: '2023-01-01T00:00:01Z', + loading: false, + artifacts: [ { - timeline_item_type: 'internal_code', - title: 'Solution title', - code_snippet_and_analysis: 'Solution text', + key: 'solution', + reason: 'Solution plan', + data: { + one_line_summary: 'Solution title', + steps: [{title: 'Fix it', description: 'Solution text'}], + }, }, ], - solution_selected: true, }, ], }; @@ -379,10 +379,17 @@ describe('useCopyIssueDetails', () => { isPending: false, }); - jest.spyOn(autofixHooks, 'useAutofixData').mockReturnValue({ - data: mockAutofixData, - isPending: false, - }); + jest.spyOn(explorerAutofixHooks, 'useExplorerAutofix').mockReturnValue({ + runState: mockAutofixData, + isLoading: false, + isPolling: false, + startStep: jest.fn(), + createPR: jest.fn(), + reset: jest.fn(), + triggerCodingAgentHandoff: jest.fn(), + codingAgentErrors: [], + dismissCodingAgentError: jest.fn(), + } as any); jest.spyOn(indicators, 'addSuccessMessage').mockImplementation(() => {}); jest.spyOn(indicators, 'addErrorMessage').mockImplementation(() => {}); diff --git a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx index 85bd72e37344cd..4b2b1e09d7ea51 100644 --- a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx +++ b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx @@ -2,12 +2,15 @@ import {useCallback, useMemo, useSyncExternalStore} from 'react'; import {useHotkeys} from '@sentry/scraps/hotkey'; -import type {AutofixData} from 'sentry/components/events/autofix/types'; -import {useAutofixData} from 'sentry/components/events/autofix/useAutofix'; import { - getRootCauseCopyText, - getSolutionCopyText, -} from 'sentry/components/events/autofix/utils'; + type ExplorerAutofixState, + getAutofixArtifactFromSection, + getOrderedAutofixSections, + isRootCauseSection, + isSolutionSection, + useExplorerAutofix, +} from 'sentry/components/events/autofix/useExplorerAutofix'; +import {artifactToMarkdown} from 'sentry/components/events/autofix/v3/utils'; import { useGroupSummaryData, type GroupSummaryData, @@ -142,7 +145,7 @@ export const issueAndEventToMarkdown = ( group: Group, event: Event | null | undefined, groupSummaryData: GroupSummaryData | null | undefined, - autofixData: AutofixData | null | undefined, + autofixData: ExplorerAutofixState | null | undefined, activeThreadId: number | undefined ): string => { // Format the basic issue information @@ -169,8 +172,23 @@ export const issueAndEventToMarkdown = ( } if (autofixData) { - const rootCauseCopyText = getRootCauseCopyText(autofixData); - const solutionCopyText = getSolutionCopyText(autofixData); + const sections = getOrderedAutofixSections(autofixData); + const rootCauseSection = sections.find(isRootCauseSection); + const solutionSection = sections.find(isSolutionSection); + + const rootCauseArtifact = rootCauseSection + ? getAutofixArtifactFromSection(rootCauseSection) + : null; + const solutionArtifact = solutionSection + ? getAutofixArtifactFromSection(solutionSection) + : null; + + const rootCauseCopyText = rootCauseArtifact + ? artifactToMarkdown(rootCauseArtifact) + : null; + const solutionCopyText = solutionArtifact + ? artifactToMarkdown(solutionArtifact) + : null; if (rootCauseCopyText) { markdownText += `\n## Root Cause\n\`\`\`\n${rootCauseCopyText}\n\`\`\`\n`; @@ -190,9 +208,8 @@ export const issueAndEventToMarkdown = ( export const useCopyIssueDetails = (group: Group, event?: Event) => { const organization = useOrganization(); - // These aren't guarded by useAiConfig because they are both non fetching, and should only return data when it's fetched elsewhere. const {data: groupSummaryData} = useGroupSummaryData(group); - const {data: autofixData} = useAutofixData({groupId: group.id}); + const {runState: autofixData} = useExplorerAutofix(group.id); const activeThreadId = useActiveThreadId(); const text = useMemo(() => { From b3989a82655459c59a5458b1eef677a2d006ca8e Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 22 May 2026 14:43:40 -0400 Subject: [PATCH 2/3] add heading level --- .../components/events/autofix/v3/drawer.tsx | 2 +- .../app/components/events/autofix/v3/utils.ts | 75 +++++++++++++------ .../streamline/eventNavigation.tsx | 2 +- .../streamline/hooks/useCopyIssueDetails.tsx | 10 +-- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/static/app/components/events/autofix/v3/drawer.tsx b/static/app/components/events/autofix/v3/drawer.tsx index 72facd29e2323e..b3952c1671f96c 100644 --- a/static/app/components/events/autofix/v3/drawer.tsx +++ b/static/app/components/events/autofix/v3/drawer.tsx @@ -96,7 +96,7 @@ function useHandleCopyMarkdown({ const markdown = getOrderedAutofixSections(aiAutofix.runState) .map(getAutofixArtifactFromSection) .filter(defined) - .map(artifactToMarkdown) + .map(artifact => artifactToMarkdown(artifact)) .filter(defined) .join('\n\n'); copy(markdown, {successMessage: t('Analysis copied to clipboard.')}); diff --git a/static/app/components/events/autofix/v3/utils.ts b/static/app/components/events/autofix/v3/utils.ts index 1895bc95562479..d1256664d5071a 100644 --- a/static/app/components/events/autofix/v3/utils.ts +++ b/static/app/components/events/autofix/v3/utils.ts @@ -17,44 +17,51 @@ import { type RepoPRState, } from 'sentry/views/seerExplorer/types'; -export function artifactToMarkdown(artifact: AutofixArtifact): string | null { +export function artifactToMarkdown( + artifact: AutofixArtifact, + headingLevel: 1 | 2 | 3 = 1 +): string | null { if (isRootCauseArtifact(artifact)) { - return rootCauseArtifactToMarkdown(artifact); + return rootCauseArtifactToMarkdown(artifact, headingLevel); } if (isSolutionArtifact(artifact)) { - return solutionArtifactToMarkdown(artifact); + return solutionArtifactToMarkdown(artifact, headingLevel); } if (isCodeChangesArtifact(artifact)) { - return filePatchesToMarkdown(artifact); + return filePatchesToMarkdown(artifact, headingLevel); } if (isPullRequestsArtifact(artifact)) { - return repoPRStatesToMarkdown(artifact); + return repoPRStatesToMarkdown(artifact, headingLevel); } if (isCodingAgentsArtifact(artifact)) { - return codingAgentsToMarkdown(artifact); + return codingAgentsToMarkdown(artifact, headingLevel); } return null; // unknown artifact } function rootCauseArtifactToMarkdown( - artifact: Artifact + artifact: Artifact, + headingLevel: number ): string | null { const rootCause = artifact.data; if (!defined(rootCause)) { return null; } - const parts: string[] = ['# Root Cause', '', rootCause.one_line_description]; + const h1 = '#'.repeat(headingLevel); + const h2 = '#'.repeat(headingLevel + 1); + + const parts: string[] = [`${h1} Root Cause`, '', rootCause.one_line_description]; if (rootCause.five_whys.length) { parts.push( '', - '## Why did this happen?', + `${h2} Why did this happen?`, '', ...rootCause.five_whys.map(why => `- ${why}`) ); @@ -63,7 +70,7 @@ function rootCauseArtifactToMarkdown( if (rootCause.reproduction_steps?.length) { parts.push( '', - '## Reproduction Steps', + `${h2} Reproduction Steps`, '', ...rootCause.reproduction_steps.map((step, index) => `${index + 1}. ${step}`) ); @@ -72,21 +79,28 @@ function rootCauseArtifactToMarkdown( return parts.join('\n'); } -function solutionArtifactToMarkdown(artifact: Artifact): string | null { +function solutionArtifactToMarkdown( + artifact: Artifact, + headingLevel: number +): string | null { const solution = artifact.data; if (!defined(solution)) { return null; } - const parts: string[] = ['# Plan', '', solution.one_line_summary]; + const h1 = '#'.repeat(headingLevel); + const h2 = '#'.repeat(headingLevel + 1); + const h3 = '#'.repeat(headingLevel + 2); + + const parts: string[] = [`${h1} Plan`, '', solution.one_line_summary]; if (solution.steps.length) { parts.push( '', - '## Steps to Resolve', + `${h2} Steps to Resolve`, '', ...solution.steps.flatMap((step, index) => [ - `### ${index + 1}. ${step.title}`, + `${h3} ${index + 1}. ${step.title}`, step.description, ]) ); @@ -95,17 +109,23 @@ function solutionArtifactToMarkdown(artifact: Artifact): strin return parts.join('\n'); } -function filePatchesToMarkdown(artifact: ExplorerFilePatch[]): string | null { +function filePatchesToMarkdown( + artifact: ExplorerFilePatch[], + headingLevel: number +): string | null { if (!artifact.length) { return null; } - const parts: string[] = ['# Code Changes']; + const h1 = '#'.repeat(headingLevel); + const h2 = '#'.repeat(headingLevel + 1); + + const parts: string[] = [`${h1} Code Changes`]; parts.push( ...artifact.flatMap(filePatch => [ '', - `## Repository: ${filePatch.repo_name}`, + `${h2} Repository: ${filePatch.repo_name}`, '', '```diff', filePatch.diff, @@ -116,12 +136,17 @@ function filePatchesToMarkdown(artifact: ExplorerFilePatch[]): string | null { return parts.join('\n'); } -function repoPRStatesToMarkdown(artifact: RepoPRState[]): string | null { +function repoPRStatesToMarkdown( + artifact: RepoPRState[], + headingLevel: number +): string | null { if (!artifact.length) { return null; } - const parts: string[] = ['# Pull Requests', '']; + const h1 = '#'.repeat(headingLevel); + + const parts: string[] = [`${h1} Pull Requests`, '']; parts.push( ...artifact @@ -137,12 +162,18 @@ function repoPRStatesToMarkdown(artifact: RepoPRState[]): string | null { return parts.join('\n'); } -function codingAgentsToMarkdown(artifact: ExplorerCodingAgentState[]): string | null { +function codingAgentsToMarkdown( + artifact: ExplorerCodingAgentState[], + headingLevel: number +): string | null { if (!artifact.length) { return null; } - const parts: string[] = ['# Coding Agents', '']; + const h1 = '#'.repeat(headingLevel); + const h2 = '#'.repeat(headingLevel + 1); + + const parts: string[] = [`${h1} Coding Agents`, '']; parts.push( ...artifact @@ -152,7 +183,7 @@ function codingAgentsToMarkdown(artifact: ExplorerCodingAgentState[]): string | } return [ - `## ${getCodingAgentName(codingAgent.provider)}`, + `${h2} ${getCodingAgentName(codingAgent.provider)}`, '', `[${codingAgent.name}](${codingAgent.agent_url})`, ]; diff --git a/static/app/views/issueDetails/streamline/eventNavigation.tsx b/static/app/views/issueDetails/streamline/eventNavigation.tsx index cbb1dcb5818a78..601e6e159ff7b3 100644 --- a/static/app/views/issueDetails/streamline/eventNavigation.tsx +++ b/static/app/views/issueDetails/streamline/eventNavigation.tsx @@ -117,7 +117,7 @@ export function IssueEventNavigation({event, group}: IssueEventNavigationProps) // Get data for markdown copy functionality const {data: groupSummaryData} = useGroupSummaryData(group); - const {runState: autofixData} = useExplorerAutofix(group.id); + const {runState: autofixData} = useExplorerAutofix(group.id, {enabled: false}); const handleCopyMarkdown = useCallback(() => { const markdownText = issueAndEventToMarkdown( diff --git a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx index 4b2b1e09d7ea51..bb636df57906cc 100644 --- a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx +++ b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.tsx @@ -184,17 +184,17 @@ export const issueAndEventToMarkdown = ( : null; const rootCauseCopyText = rootCauseArtifact - ? artifactToMarkdown(rootCauseArtifact) + ? artifactToMarkdown(rootCauseArtifact, 2) : null; const solutionCopyText = solutionArtifact - ? artifactToMarkdown(solutionArtifact) + ? artifactToMarkdown(solutionArtifact, 2) : null; if (rootCauseCopyText) { - markdownText += `\n## Root Cause\n\`\`\`\n${rootCauseCopyText}\n\`\`\`\n`; + markdownText += `\n${rootCauseCopyText}\n`; } if (solutionCopyText) { - markdownText += `\n## Solution\n\`\`\`\n${solutionCopyText}\n\`\`\`\n`; + markdownText += `\n${solutionCopyText}\n`; } } @@ -209,7 +209,7 @@ export const useCopyIssueDetails = (group: Group, event?: Event) => { const organization = useOrganization(); const {data: groupSummaryData} = useGroupSummaryData(group); - const {runState: autofixData} = useExplorerAutofix(group.id); + const {runState: autofixData} = useExplorerAutofix(group.id, {enabled: false}); const activeThreadId = useActiveThreadId(); const text = useMemo(() => { From 81bfb6be5ae0107356e4e3a0f330fd87f90abd94 Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 22 May 2026 14:55:55 -0400 Subject: [PATCH 3/3] fix tests --- .../streamline/hooks/useCopyIssueDetails.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx index 613e30066ec914..dc7e69d152585a 100644 --- a/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx +++ b/static/app/views/issueDetails/streamline/hooks/useCopyIssueDetails.spec.tsx @@ -124,7 +124,7 @@ describe('useCopyIssueDetails', () => { ); expect(result).toContain('## Root Cause'); - expect(result).toContain('## Solution'); + expect(result).toContain('## Plan'); }); it('includes tags when present in event', () => { @@ -437,7 +437,7 @@ describe('useCopyIssueDetails', () => { expect(capturedText).toContain(`**Project:** ${group.project?.slug}`); expect(capturedText).toContain('## Issue Summary'); expect(capturedText).toContain('## Root Cause'); - expect(capturedText).toContain('## Solution'); + expect(capturedText).toContain('## Plan'); expect(capturedText).not.toContain('## Exception'); });