From b3e740eb1cd21558b6e48c360f6155577c080bc4 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 10 Nov 2026 15:37:53 -0800 Subject: [PATCH 1/3] ref(autofix): Match the running Autofix tab to the Seer Agent chat Render the in-progress Autofix card with the chat's ResponseGroup and ThinkingBlock, so a running Autofix looks the same in the issue tab and in the Seer Agent panel. Also hide the sidebar's Seer Autofix section while the panel shows this issue's single-run Autofix chat, since the panel already shows the same run. --- .../autofix/v3/artifactLoadingDetails.tsx | 85 +++++++------------ .../events/autofix/v3/autofixCards.spec.tsx | 64 +++++++------- .../autofix/v3/useToggleSingleRunChat.tsx | 14 ++- .../sidebar/autofixSection.spec.tsx | 48 ++++++++++- .../issueDetails/sidebar/autofixSection.tsx | 22 ++++- 5 files changed, 145 insertions(+), 88 deletions(-) diff --git a/static/app/components/events/autofix/v3/artifactLoadingDetails.tsx b/static/app/components/events/autofix/v3/artifactLoadingDetails.tsx index 1edf10680c60..21f2a3ae946c 100644 --- a/static/app/components/events/autofix/v3/artifactLoadingDetails.tsx +++ b/static/app/components/events/autofix/v3/artifactLoadingDetails.tsx @@ -1,11 +1,13 @@ -import {Spinner} from '@sentry/scraps/chat'; -import {Flex, Stack} from '@sentry/scraps/layout'; -import {Text} from '@sentry/scraps/text'; +import {useMemo, useState} from 'react'; + +import {MessageRow, ThinkingBlock} from '@sentry/scraps/chat'; import {type AutofixSection} from 'sentry/components/events/autofix/useExplorerAutofix'; -import {SeerMarkdown} from 'sentry/components/seer/markdown'; -import {useAutoScroll} from 'sentry/utils/useAutoScroll'; -import {hasValidContent} from 'sentry/views/seerExplorer/components/chat/shared'; +import { + groupTranscript, + ResponseGroup, +} from 'sentry/views/seerExplorer/components/chat/responseGroup'; +import {findLatestTodos} from 'sentry/views/seerExplorer/components/chat/toolUse'; import {ArtifactDetails} from './artifactDetails'; @@ -18,55 +20,34 @@ export function ArtifactLoadingDetails({ loadingMessage, blocks, }: ArtifactLoadingDetailsProps) { - const {containerRef, onScrollHandler} = useAutoScroll({key: blocks}); + const [appearedAt] = useState(() => new Date()); + const responses = useMemo( + () => + groupTranscript(blocks).flatMap(segment => + segment.kind === 'response' ? [segment] : [] + ), + [blocks] + ); + const latestTodos = useMemo(() => findLatestTodos(blocks), [blocks]); return ( - - {blocks.map((block, index) => { - if (block.message.role === 'user') { - // The user role is used to pass the prompts - return null; - } - - // A block still being written can end mid-embed; streaming hides the - // unclosed tag until its closer arrives instead of flashing raw syntax. - const variant = block.loading ? 'streaming' : 'static'; - - if ( - hasValidContent(block.message.content) && - block.message.content !== 'Thinking...' - ) { - return ( - - ); - } - - if (hasValidContent(block.message.thinking_content)) { - return ( - - ); - } - - return null; - })} - - - - {loadingMessage} - - - + {responses.length ? ( + responses.map(segment => ( + + )) + ) : ( + + + + )} ); } diff --git a/static/app/components/events/autofix/v3/autofixCards.spec.tsx b/static/app/components/events/autofix/v3/autofixCards.spec.tsx index a6acc909ed43..94dcfde2ff6f 100644 --- a/static/app/components/events/autofix/v3/autofixCards.spec.tsx +++ b/static/app/components/events/autofix/v3/autofixCards.spec.tsx @@ -314,6 +314,33 @@ describe('ArtifactCard', () => { expect(screen.getByText('Null pointer in user handler')).toBeInTheDocument(); }); + it('shows the agent trace in a thinking block while running', () => { + render( + + ); + + expect(screen.getByRole('button', {name: /Thinking/})).toBeInTheDocument(); + expect( + screen.getByText('Files are under python/. Let me read them.') + ).toBeInTheDocument(); + expect(screen.queryByText('Thinking...')).not.toBeInTheDocument(); + }); + it('starts a new single run from Re-run', async () => { const startStepMock = jest.fn().mockResolvedValue({runId: 777, isSingleRun: true}); render( @@ -856,7 +883,7 @@ describe('ArtifactCard', () => { /> ); - expect(screen.getByText('Implementing changes…')).toBeInTheDocument(); + expect(screen.getByText('Implementing changes')).toBeInTheDocument(); expect( screen.queryByText( 'Seer failed to generate a code change. This one is on us. Try running it again.' @@ -864,29 +891,6 @@ describe('ArtifactCard', () => { ).not.toBeInTheDocument(); }); - it('hides an unclosed embed tag in a block that is still loading', () => { - render( - - ); - - expect(screen.getByText(/Looking at/)).toBeInTheDocument(); - expect(screen.queryByText(/\{%/)).not.toBeInTheDocument(); - }); - it('does not render file diff viewers in error state', () => { render( { ); expect(screen.getByText('1 file changed in 1 repo')).toBeInTheDocument(); - expect(screen.queryByText('Iterating on PR…')).not.toBeInTheDocument(); - expect(screen.queryByText('Implementing changes…')).not.toBeInTheDocument(); + expect(screen.queryByText('Iterating on PR')).not.toBeInTheDocument(); + expect(screen.queryByText('Implementing changes')).not.toBeInTheDocument(); }); it('renders queued feedback as a feedback item', () => { @@ -1882,8 +1886,8 @@ describe('ArtifactCard', () => { ); expect(screen.getByText('1 file changed in 1 repo')).toBeInTheDocument(); - expect(screen.queryByText('Implementing changes…')).not.toBeInTheDocument(); - expect(screen.queryByText('Iterating on PR…')).not.toBeInTheDocument(); + expect(screen.queryByText('Implementing changes')).not.toBeInTheDocument(); + expect(screen.queryByText('Iterating on PR')).not.toBeInTheDocument(); }); it('renders a one-based version tag for the latest iteration', () => { @@ -1935,8 +1939,8 @@ describe('ArtifactCard', () => { /> ); - expect(screen.getByText('Iterating on PR…')).toBeInTheDocument(); - expect(screen.queryByText('Implementing changes…')).not.toBeInTheDocument(); + expect(screen.getByText('Iterating on PR')).toBeInTheDocument(); + expect(screen.queryByText('Implementing changes')).not.toBeInTheDocument(); }); it('marks block feedback as processed when the section is not processing', () => { diff --git a/static/app/components/events/autofix/v3/useToggleSingleRunChat.tsx b/static/app/components/events/autofix/v3/useToggleSingleRunChat.tsx index 82f04d828356..43b6a11d3960 100644 --- a/static/app/components/events/autofix/v3/useToggleSingleRunChat.tsx +++ b/static/app/components/events/autofix/v3/useToggleSingleRunChat.tsx @@ -20,19 +20,25 @@ export function useOpenSingleRunChat() { ); } -export function useToggleSingleRunChat(runId: SeerExplorerRunId | undefined) { - const {closeSeerExplorer, isOpen} = useSeerExplorerContext(); +export function useIsSingleRunChatOpen(runId: SeerExplorerRunId | undefined) { + const {isOpen} = useSeerExplorerContext(); const {runId: openRunId} = useSeerExplorerChatState(); + return runId !== undefined && isOpen && String(openRunId) === String(runId); +} + +export function useToggleSingleRunChat(runId: SeerExplorerRunId | undefined) { + const {closeSeerExplorer} = useSeerExplorerContext(); + const isChatOpen = useIsSingleRunChatOpen(runId); const openSingleRunChat = useOpenSingleRunChat(); return useCallback(() => { if (runId === undefined) { return; } - if (isOpen && String(openRunId) === String(runId)) { + if (isChatOpen) { closeSeerExplorer(); } else { openSingleRunChat(runId); } - }, [runId, isOpen, openRunId, openSingleRunChat, closeSeerExplorer]); + }, [runId, isChatOpen, openSingleRunChat, closeSeerExplorer]); } diff --git a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx index f4fa30a273db..49c715dac171 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx @@ -21,9 +21,19 @@ import type {LLMContextSnapshot} from 'sentry/views/seerExplorer/contexts/llmCon import {AutofixSection} from './autofixSection'; const mockOpenSeerExplorer = jest.fn(); +let mockIsExplorerOpen = false; jest.mock('sentry/views/seerExplorer/useSeerExplorerContext', () => ({ ...jest.requireActual('sentry/views/seerExplorer/useSeerExplorerContext'), - useSeerExplorerContext: () => ({openSeerExplorer: mockOpenSeerExplorer}), + useSeerExplorerContext: () => ({ + openSeerExplorer: mockOpenSeerExplorer, + isOpen: mockIsExplorerOpen, + }), +})); + +let mockOpenRunId: number | null = null; +jest.mock('sentry/views/seerExplorer/seerExplorerChatStateContext', () => ({ + ...jest.requireActual('sentry/views/seerExplorer/seerExplorerChatStateContext'), + useSeerExplorerChatState: () => ({runId: mockOpenRunId}), })); jest.mock('sentry/utils/cells'); @@ -209,6 +219,10 @@ describe('AutofixSection', () => { it('starts collapsed on the autofix tab even when saved as open', async () => { mockUseMatches.mockImplementation(() => matchesForTab(Tab.AUTOFIX)); localStorage.setItem('issue-details-fold-section-collapse:seer', 'false'); + MockApiClient.addMockResponse({ + url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`, + body: {autofix: null}, + }); render(, { organization, @@ -298,6 +312,38 @@ describe('AutofixSection', () => { beforeEach(() => { mockOpenSeerExplorer.mockClear(); + mockIsExplorerOpen = false; + mockOpenRunId = null; + }); + + it('hides the section while the run is open in the Explorer panel', async () => { + mockIsExplorerOpen = true; + mockOpenRunId = 776; + const request = MockApiClient.addMockResponse({ + url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`, + body: singleRunBody, + }); + + render(, {organization}); + + await waitFor(() => expect(request).toHaveBeenCalled()); + await waitFor(() => + expect(screen.queryByText('Seer Autofix')).not.toBeInTheDocument() + ); + }); + + it('keeps the section while the Explorer panel shows another chat', async () => { + mockIsExplorerOpen = true; + mockOpenRunId = 123; + MockApiClient.addMockResponse({ + url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`, + body: singleRunBody, + }); + + render(, {organization}); + + expect(await screen.findByText('Divides by zero')).toBeInTheDocument(); + expect(screen.getByText('Seer Autofix')).toBeInTheDocument(); }); it('opens the Explorer panel on Open Autofix', async () => { diff --git a/static/app/views/issueDetails/sidebar/autofixSection.tsx b/static/app/views/issueDetails/sidebar/autofixSection.tsx index 7e8b4a838e95..3273650248ea 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.tsx @@ -1,5 +1,6 @@ import {useMemo} from 'react'; import {IconSeer} from '@sentry/icons/seer'; +import {useQuery} from '@tanstack/react-query'; import {Button} from '@sentry/scraps/button'; import {Flex, Stack} from '@sentry/scraps/layout'; @@ -8,6 +9,7 @@ import {Text} from '@sentry/scraps/text'; import {getAutofixRunId} from 'sentry/components/events/autofix/autofixRunId'; import { type AutofixSection, + explorerAutofixApiOptions, getAutofixArtifactFromSection, getOrderedAutofixSections, isCodeChangesArtifact, @@ -35,7 +37,10 @@ import { } from 'sentry/components/events/autofix/v3/autofixSetupCard'; import {AutofixStartCard} from 'sentry/components/events/autofix/v3/autofixStartCard'; import {useAutoTriggerAutofix} from 'sentry/components/events/autofix/v3/useAutoTriggerAutofix'; -import {useToggleSingleRunChat} from 'sentry/components/events/autofix/v3/useToggleSingleRunChat'; +import { + useIsSingleRunChatOpen, + useToggleSingleRunChat, +} from 'sentry/components/events/autofix/v3/useToggleSingleRunChat'; import {artifactToMarkdown} from 'sentry/components/events/autofix/v3/utils'; import {OverrideOrDefault} from 'sentry/components/overrideOrDefault'; import {Placeholder} from 'sentry/components/placeholder'; @@ -45,6 +50,7 @@ import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils/defined'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; +import {useOrganization} from 'sentry/utils/useOrganization'; import {SectionKey} from 'sentry/views/issueDetails/context'; import {SidebarFoldSection} from 'sentry/views/issueDetails/foldSection'; import {useAiConfig} from 'sentry/views/issueDetails/hooks/useAiConfig'; @@ -64,6 +70,7 @@ interface AutofixSectionProps { } export function AutofixSection({group, project}: AutofixSectionProps) { + const organization = useOrganization(); const aiConfig = useAiConfig(group, project); const isOnAutofixTab = useCurrentTab() === Tab.AUTOFIX; @@ -72,6 +79,15 @@ export function AutofixSection({group, project}: AutofixSectionProps) { const issueTypeSupportsSeer = issueTypeConfig.autofix || issueTypeConfig.issueSummary.enabled; + const {data} = useQuery({ + ...explorerAutofixApiOptions(organization.slug, group.id), + enabled: aiConfig.areAiFeaturesAllowed && issueTypeSupportsSeer, + }); + const runState = data?.autofix ?? null; + const isAutofixChatOpen = useIsSingleRunChatOpen( + getAutofixRunId(isSingleRunAutofix(runState) ? runState : null) + ); + if (!aiConfig.areAiFeaturesAllowed || !issueTypeSupportsSeer) { if (!issueTypeConfig.resources) { return null; @@ -96,6 +112,10 @@ export function AutofixSection({group, project}: AutofixSectionProps) { ); } + if (isAutofixChatOpen) { + return null; + } + return ( Date: Wed, 11 Nov 2026 14:13:43 -0800 Subject: [PATCH 2/3] ref(autofix): Collapse the sidebar section instead of hiding it Keep the Seer Autofix section visible but collapsed while the Seer Agent panel shows this issue's single run, per design feedback. --- .../issueDetails/sidebar/autofixSection.spec.tsx | 11 +++++++---- .../app/views/issueDetails/sidebar/autofixSection.tsx | 10 ++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx index 49c715dac171..11998722d83c 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx @@ -316,20 +316,23 @@ describe('AutofixSection', () => { mockOpenRunId = null; }); - it('hides the section while the run is open in the Explorer panel', async () => { + it('collapses the section while the run is open in the Explorer panel', async () => { mockIsExplorerOpen = true; mockOpenRunId = 776; - const request = MockApiClient.addMockResponse({ + MockApiClient.addMockResponse({ url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`, body: singleRunBody, }); render(, {organization}); - await waitFor(() => expect(request).toHaveBeenCalled()); await waitFor(() => - expect(screen.queryByText('Seer Autofix')).not.toBeInTheDocument() + expect(screen.getByRole('button', {name: 'View Section'})).toHaveAttribute( + 'aria-expanded', + 'false' + ) ); + expect(screen.getByText('Seer Autofix')).toBeInTheDocument(); }); it('keeps the section while the Explorer panel shows another chat', async () => { diff --git a/static/app/views/issueDetails/sidebar/autofixSection.tsx b/static/app/views/issueDetails/sidebar/autofixSection.tsx index 3273650248ea..bb54f582834e 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.tsx @@ -112,9 +112,7 @@ export function AutofixSection({group, project}: AutofixSectionProps) { ); } - if (isAutofixChatOpen) { - return null; - } + const startCollapsed = isOnAutofixTab || isAutofixChatOpen; return ( {t('Seer Autofix')} @@ -131,8 +129,8 @@ export function AutofixSection({group, project}: AutofixSectionProps) { } sectionKey={SectionKey.SEER} preventCollapse={false} - initialCollapse={isOnAutofixTab} - disableCollapsePersistence={isOnAutofixTab} + initialCollapse={startCollapsed} + disableCollapsePersistence={startCollapsed} > From 54ce74e84bdadc771f9248907df1917e0782670b Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Wed, 11 Nov 2026 15:01:07 -0800 Subject: [PATCH 3/3] fix(autofix): Recollapse the sidebar section when the chat opens on the Autofix tab Key the fold section on both the tab and the chat state, so opening the run's chat while on the Autofix tab remounts it collapsed. --- .../sidebar/autofixSection.spec.tsx | 27 +++++++++++++++++++ .../issueDetails/sidebar/autofixSection.tsx | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx index 11998722d83c..c54e9b558b0a 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.spec.tsx @@ -335,6 +335,33 @@ describe('AutofixSection', () => { expect(screen.getByText('Seer Autofix')).toBeInTheDocument(); }); + it('collapses an expanded autofix-tab section when the run opens in the panel', async () => { + mockUseMatches.mockImplementation(() => matchesForTab(Tab.AUTOFIX)); + MockApiClient.addMockResponse({ + url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`, + body: singleRunBody, + }); + + const {rerender} = render( + , + {organization} + ); + + await userEvent.click(await screen.findByRole('button', {name: 'View Section'})); + expect(screen.getByRole('button', {name: 'Collapse Section'})).toBeInTheDocument(); + + mockIsExplorerOpen = true; + mockOpenRunId = 776; + rerender(); + + await waitFor(() => + expect(screen.getByRole('button', {name: 'View Section'})).toHaveAttribute( + 'aria-expanded', + 'false' + ) + ); + }); + it('keeps the section while the Explorer panel shows another chat', async () => { mockIsExplorerOpen = true; mockOpenRunId = 123; diff --git a/static/app/views/issueDetails/sidebar/autofixSection.tsx b/static/app/views/issueDetails/sidebar/autofixSection.tsx index bb54f582834e..7048520cf8f7 100644 --- a/static/app/views/issueDetails/sidebar/autofixSection.tsx +++ b/static/app/views/issueDetails/sidebar/autofixSection.tsx @@ -120,7 +120,7 @@ export function AutofixSection({group, project}: AutofixSectionProps) { // there. Opening it on that tab shouldn't change the saved preference // for every other tab, and the key remounts it so each tab starts from // its own state. - key={startCollapsed ? 'collapsed' : 'default'} + key={`${isOnAutofixTab}-${isAutofixChatOpen}`} title={ {t('Seer Autofix')}