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
31 changes: 29 additions & 2 deletions static/app/components/events/autofix/autofixRootCause.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('AutofixRootCause', () => {
await userEvent.click(await screen.findByText('Send to Cursor'));

expect(JSON.parse(localStorage.getItem('autofix:rootCauseActionPreference')!)).toBe(
'cursor:cursor-integration-id'
'agent:cursor-integration-id'
);
});

Expand Down Expand Up @@ -204,7 +204,7 @@ describe('AutofixRootCause', () => {

localStorage.setItem(
'autofix:rootCauseActionPreference',
JSON.stringify('cursor:cursor-integration-id')
JSON.stringify('agent:cursor-integration-id')
);

render(<AutofixRootCause {...defaultProps} />);
Expand All @@ -222,6 +222,33 @@ describe('AutofixRootCause', () => {
expect(await screen.findByText('Find Solution with Seer')).toBeInTheDocument();
});

it('shows Cursor as primary when using legacy cursor: prefix (backwards compatibility)', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/integrations/coding-agents/',
body: {
integrations: [
{
id: 'cursor-integration-id',
name: 'Cursor',
provider: 'cursor',
},
],
},
});

// Use the legacy 'cursor:' prefix that existing users may have stored
localStorage.setItem(
'autofix:rootCauseActionPreference',
JSON.stringify('cursor:cursor-integration-id')
);

render(<AutofixRootCause {...defaultProps} />);

expect(
await screen.findByRole('button', {name: 'Send to Cursor'})
).toBeInTheDocument();
});

it('both options accessible in dropdown', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/integrations/coding-agents/',
Expand Down
80 changes: 44 additions & 36 deletions static/app/components/events/autofix/autofixRootCause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ function CopyRootCauseButton({
}

function SolutionActionButton({
cursorIntegrations,
codingAgentIntegrations,
preferredAction,
primaryButtonPriority,
isSelectingRootCause,
Expand All @@ -259,18 +259,24 @@ function SolutionActionButton({
handleLaunchCodingAgent,
findSolutionTitle,
}: {
cursorIntegrations: CodingAgentIntegration[];
codingAgentIntegrations: CodingAgentIntegration[];
findSolutionTitle: string;
handleLaunchCodingAgent: (integrationId: string, integrationName: string) => void;
handleLaunchCodingAgent: (integration: CodingAgentIntegration) => void;
isLaunchingAgent: boolean;
isLoadingAgents: boolean;
isSelectingRootCause: boolean;
preferredAction: string;
primaryButtonPriority: React.ComponentProps<typeof Button>['priority'];
submitFindSolution: () => void;
}) {
const preferredIntegration = preferredAction.startsWith('cursor:')
? cursorIntegrations.find(i => i.id === preferredAction.replace('cursor:', ''))
// Support both 'agent:' (new) and 'cursor:' (legacy) prefixes for backwards compatibility
const isAgentPreference =
preferredAction.startsWith('agent:') || preferredAction.startsWith('cursor:');
const preferredIntegration = isAgentPreference
? codingAgentIntegrations.find(i => {
const key = preferredAction.replace(/^(agent|cursor):/, '');
return i.id === key || (i.id === null && i.provider === key);
})
: null;

const effectivePreference =
Expand All @@ -282,11 +288,12 @@ function SolutionActionButton({

// Check if there are duplicate names among integrations (need to show ID to distinguish)
const hasDuplicateNames =
cursorIntegrations.length > 1 &&
new Set(cursorIntegrations.map(i => i.name)).size < cursorIntegrations.length;
codingAgentIntegrations.length > 1 &&
new Set(codingAgentIntegrations.map(i => i.name)).size <
codingAgentIntegrations.length;

// If no integrations, show simple Seer button
if (cursorIntegrations.length === 0) {
if (codingAgentIntegrations.length === 0) {
return (
<Button
size="sm"
Expand All @@ -312,28 +319,39 @@ function SolutionActionButton({
},
]),
// Show all integrations except the currently preferred one
...cursorIntegrations
.filter(integration => `cursor:${integration.id}` !== effectivePreference)
...codingAgentIntegrations
.filter(integration => {
// Compare by key to handle both 'agent:' and legacy 'cursor:' prefixes
const integrationKey = integration.id ?? integration.provider;
const effectiveKey = effectivePreference.replace(/^(agent|cursor):/, '');
return integrationKey !== effectiveKey;
})
.map(integration => ({
key: `cursor:${integration.id}`,
key: `agent:${integration.id ?? integration.provider}`,
label: (
<Flex gap="md" align="center">
<PluginIcon pluginId="cursor" size={20} />
<PluginIcon pluginId={integration.provider} size={20} />
<div>{t('Send to %s', integration.name)}</div>
{hasDuplicateNames && (
<SmallIntegrationIdText>({integration.id})</SmallIntegrationIdText>
<SmallIntegrationIdText>
({integration.id ?? integration.provider})
</SmallIntegrationIdText>
)}
</Flex>
),
onAction: () => handleLaunchCodingAgent(integration.id, integration.name),
onAction: () => handleLaunchCodingAgent(integration),
disabled: isLoadingAgents || isLaunchingAgent,
})),
];

const primaryButtonLabel = isSeerPreferred
? t('Find Solution with Seer')
: hasDuplicateNames
? t('Send to %s (%s)', preferredIntegration!.name, preferredIntegration!.id)
? t(
'Send to %s (%s)',
preferredIntegration!.name,
preferredIntegration!.id ?? preferredIntegration!.provider
)
: t('Send to %s', preferredIntegration!.name);

const primaryButtonProps = isSeerPreferred
Expand All @@ -344,10 +362,9 @@ function SolutionActionButton({
children: primaryButtonLabel,
}
: {
onClick: () =>
handleLaunchCodingAgent(preferredIntegration!.id, preferredIntegration!.name),
onClick: () => handleLaunchCodingAgent(preferredIntegration!),
busy: isLaunchingAgent,
icon: <PluginIcon pluginId="cursor" size={16} />,
icon: <PluginIcon pluginId={preferredIntegration!.provider} size={16} />,
children: primaryButtonLabel,
};

Expand Down Expand Up @@ -414,15 +431,15 @@ function AutofixRootCauseDisplay({
runId
);

// Stores 'seer_solution' or an integration ID (e.g., 'cursor:123')
// Stores 'seer_solution' or an integration ID (e.g., 'agent:123')
const [preferredAction, setPreferredAction] = useLocalStorageState<string>(
'autofix:rootCauseActionPreference',
'seer_solution'
);

// Simulate a click on the description to trigger the text selection
const handleSelectDescription = () => {
if (descriptionRef.current) {
// Simulate a click on the description to trigger the text selection
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
Expand Down Expand Up @@ -463,29 +480,20 @@ function AutofixRootCauseDisplay({
});
};

const cursorIntegrations = codingAgentIntegrations.filter(
integration => integration.provider === 'cursor'
);

const handleLaunchCodingAgent = (integrationId: string, integrationName: string) => {
const targetIntegration = cursorIntegrations.find(i => i.id === integrationId);

if (!targetIntegration) {
return;
}

const handleLaunchCodingAgent = (integration: CodingAgentIntegration) => {
// Save user preference with specific integration ID
setPreferredAction(`cursor:${integrationId}`);
setPreferredAction(`agent:${integration.id ?? integration.provider}`);

addLoadingMessage(t('Launching %s...', integrationName), {
addLoadingMessage(t('Launching %s...', integration.name), {
duration: 60000,
});

const instruction = solutionText.trim();

launchCodingAgent({
integrationId: targetIntegration.id,
agentName: targetIntegration.name,
integrationId: integration.id,
provider: integration.provider,
agentName: integration.name,
triggerSource: 'root_cause',
instruction: instruction || undefined,
});
Expand Down Expand Up @@ -619,7 +627,7 @@ function AutofixRootCauseDisplay({
<ButtonBar>
<CopyRootCauseButton cause={cause} event={event} />
<SolutionActionButton
cursorIntegrations={cursorIntegrations}
codingAgentIntegrations={codingAgentIntegrations}
preferredAction={preferredAction}
primaryButtonPriority={primaryButtonPriority}
isSelectingRootCause={isSelectingRootCause}
Expand Down
9 changes: 7 additions & 2 deletions static/app/components/events/autofix/codingAgentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import styled from '@emotion/styled';
import {AnimatePresence, motion, type MotionNodeAnimationOptions} from 'framer-motion';

import {Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

import {Tag, type TagProps} from 'sentry/components/core/badge/tag';
import {Button} from 'sentry/components/core/button';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import {ExternalLink} from 'sentry/components/core/link';
import {Text} from 'sentry/components/core/text';
import {DateTime} from 'sentry/components/dateTime';
import {
CodingAgentProvider,
Expand Down Expand Up @@ -71,6 +71,8 @@ function CodingAgentCard({codingAgentState, repo}: CodingAgentCardProps) {
switch (provider) {
case CodingAgentProvider.CURSOR_BACKGROUND_AGENT:
return t('Cursor Cloud Agent');
case CodingAgentProvider.GITHUB_COPILOT_AGENT:
return t('GitHub Copilot');
default:
return t('Coding Agent');
}
Expand Down Expand Up @@ -165,7 +167,10 @@ function CodingAgentCard({codingAgentState, repo}: CodingAgentCardProps) {
analyticsEventName="Autofix: Open Coding Agent"
analyticsEventKey="autofix.coding_agent.open"
>
{t('Open in Cursor')}
{codingAgentState.provider ===
CodingAgentProvider.CURSOR_BACKGROUND_AGENT
? t('Open in Cursor')
: t('View Agent')}
</Button>
</ExternalLink>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function CursorIntegrationCta({project}: CursorIntegrationCtaProps) {
const isConfigured = Boolean(preference?.automation_handoff) && isAutomationEnabled;

const handleSetupClick = useCallback(async () => {
if (!cursorIntegration) {
if (!cursorIntegration?.id) {
throw new Error('Cursor integration not found');
}

Expand Down
111 changes: 111 additions & 0 deletions static/app/components/events/autofix/githubCopilotIntegrationCta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {Container, Flex} from '@sentry/scraps/layout';
import {Heading, Text} from '@sentry/scraps/text';

import {LinkButton} from 'sentry/components/core/button/linkButton';
import {ExternalLink} from 'sentry/components/core/link';
import {useCodingAgentIntegrations} from 'sentry/components/events/autofix/useAutofix';
import Placeholder from 'sentry/components/placeholder';
import {t, tct} from 'sentry/locale';
import {PluginIcon} from 'sentry/plugins/components/pluginIcon';
import useOrganization from 'sentry/utils/useOrganization';

export function GithubCopilotIntegrationCta() {
const organization = useOrganization();

const {data: codingAgentIntegrations, isLoading: isLoadingIntegrations} =
useCodingAgentIntegrations();

const githubCopilotIntegration = codingAgentIntegrations?.integrations.find(
integration => integration.provider === 'github_copilot'
);

const hasGithubCopilotFeatureFlag = organization.features.includes(
'integrations-github-copilot-agent'
);
const hasGithubCopilotIntegration = Boolean(githubCopilotIntegration);

if (!hasGithubCopilotFeatureFlag) {
return null;
}

if (isLoadingIntegrations) {
return (
<Container
padding="xl"
border="primary"
radius="md"
marginTop="2xl"
marginBottom="2xl"
>
<Placeholder height="120px" />
</Container>
);
}

if (!hasGithubCopilotIntegration) {
return (
<Container
padding="xl"
border="primary"
radius="md"
marginTop="2xl"
marginBottom="2xl"
>
<Flex direction="column" gap="lg">
<Heading as="h3">
<Flex direction="row" gap="sm" align="center">
<PluginIcon pluginId="github" /> <span>GitHub Copilot Integration</span>
</Flex>
</Heading>
<Text>
{tct(
'Connect GitHub Copilot to hand off Seer root cause analysis to GitHub Copilot coding agent for seamless code fixes. [docsLink:Read the docs] to learn more.',
{
docsLink: (
<ExternalLink href="https://docs.sentry.io/organization/integrations/github-copilot/" />
),
}
)}
</Text>
<div>
<LinkButton
to={`/settings/${organization.slug}/integrations/github_copilot/`}
priority="default"
size="sm"
>
{t('Install GitHub Copilot Integration')}
</LinkButton>
</div>
</Flex>
</Container>
);
}

return (
<Container
padding="xl"
border="primary"
radius="md"
marginTop="2xl"
marginBottom="2xl"
>
<Flex direction="column" gap="lg">
<Heading as="h3">
<Flex direction="row" gap="sm" align="center">
<PluginIcon pluginId="github" /> <span>GitHub Copilot Integration</span>
</Flex>
</Heading>
<Text>
{tct(
'GitHub Copilot integration is installed. You can trigger GitHub Copilot from Issue Fix to create pull requests. [docsLink:Read the docs] to learn more.',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update this copy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(can be done in a follow up, the flag is not on for anyone atm)

{
docsLink: (
<ExternalLink href="https://docs.sentry.io/organization/integrations/github-copilot/" />
),
}
)}
</Text>
</Flex>
</Container>
);
}
1 change: 1 addition & 0 deletions static/app/components/events/autofix/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export enum CodingAgentStatus {

export enum CodingAgentProvider {
CURSOR_BACKGROUND_AGENT = 'cursor_background_agent',
GITHUB_COPILOT_AGENT = 'github_copilot_agent',
}

export interface CodingAgentState {
Expand Down
Loading
Loading