Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 (
<ArtifactDetails>
<Stack
gap="md"
ref={containerRef}
maxHeight="200px"
overflowY="auto"
onScroll={onScrollHandler}
>
{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 (
<SeerMarkdown key={index} raw={block.message.content} variant={variant} />
);
}

if (hasValidContent(block.message.thinking_content)) {
return (
<SeerMarkdown
key={index}
raw={block.message.thinking_content}
variant={variant}
/>
);
}

return null;
})}
<Flex align="center" gap="md">
<Spinner />
<Text variant="muted" density="compressed">
{loadingMessage}
</Text>
</Flex>
</Stack>
{responses.length ? (
responses.map(segment => (
<ResponseGroup
key={segment.indices[0]}
group={segment.blocks}
blockIndex={segment.indices[0]!}
latestTodos={latestTodos}
readOnly
showThinking
/>
))
) : (
<MessageRow from="assistant" density="compact">
<ThinkingBlock title={loadingMessage} startTime={appearedAt} />
</MessageRow>
)}
</ArtifactDetails>
);
}
64 changes: 34 additions & 30 deletions static/app/components/events/autofix/v3/autofixCards.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<RootCauseCard
autofix={mockAutofix}
groupId="1"
section={makeSection(
'root_cause',
'processing',
[],
[
{
...makeAssistantBlock('Files are under python/. Let me read them.'),
id: 'block-1',
},
{...makeAssistantBlock('Thinking...'), id: 'block-2', loading: true},
]
)}
/>
);

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(
Expand Down Expand Up @@ -856,37 +883,14 @@ 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.'
)
).not.toBeInTheDocument();
});

it('hides an unclosed embed tag in a block that is still loading', () => {
render(
<CodeChangesCard
groupId="1"
autofix={mockAutofix}
section={makeSection(
'code_changes',
'processing',
[],
[
{
...makeAssistantBlock('Looking at {% issue %}{"id":"1"'),
loading: true,
},
]
)}
/>
);

expect(screen.getByText(/Looking at/)).toBeInTheDocument();
expect(screen.queryByText(/\{%/)).not.toBeInTheDocument();
});

it('does not render file diff viewers in error state', () => {
render(
<CodeChangesCard
Expand Down Expand Up @@ -1376,8 +1380,8 @@ describe('ArtifactCard', () => {
);

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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
78 changes: 77 additions & 1 deletion static/app/views/issueDetails/sidebar/autofixSection.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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(<AutofixSection group={mockGroup} project={mockProject} />, {
organization,
Expand Down Expand Up @@ -298,6 +312,68 @@ describe('AutofixSection', () => {

beforeEach(() => {
mockOpenSeerExplorer.mockClear();
mockIsExplorerOpen = false;
mockOpenRunId = null;
});

it('collapses the section while the run is open in the Explorer panel', async () => {
mockIsExplorerOpen = true;
mockOpenRunId = 776;
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`,
body: singleRunBody,
});

render(<AutofixSection group={mockGroup} project={mockProject} />, {organization});

await waitFor(() =>
expect(screen.getByRole('button', {name: 'View Section'})).toHaveAttribute(
'aria-expanded',
'false'
)
);
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(
<AutofixSection group={mockGroup} project={mockProject} />,
{organization}
);

await userEvent.click(await screen.findByRole('button', {name: 'View Section'}));
expect(screen.getByRole('button', {name: 'Collapse Section'})).toBeInTheDocument();

mockIsExplorerOpen = true;
mockOpenRunId = 776;
rerender(<AutofixSection group={mockGroup} project={mockProject} />);

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;
MockApiClient.addMockResponse({
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`,
body: singleRunBody,
});

render(<AutofixSection group={mockGroup} project={mockProject} />, {organization});

expect(await screen.findByText('Divides by zero')).toBeInTheDocument();
expect(screen.getByText('Seer Autofix')).toBeInTheDocument();
});

it('opens the Explorer panel on Open Autofix', async () => {
Expand Down
Loading
Loading