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
10 changes: 5 additions & 5 deletions static/app/components/events/autofix/autofixRootCause.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,10 @@ describe('AutofixRootCause', () => {
await userEvent.click(dropdownTrigger);

// Click the Cursor option in the dropdown
await userEvent.click(await screen.findByText('Send to Cursor Cloud Agent'));
await userEvent.click(await screen.findByText('Send to Cursor'));

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

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

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

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

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

// Verify Seer option is in the dropdown
Expand Down Expand Up @@ -249,6 +249,6 @@ describe('AutofixRootCause', () => {
});
await userEvent.click(dropdownTrigger);

expect(await screen.findByText('Send to Cursor Cloud Agent')).toBeInTheDocument();
expect(await screen.findByText('Send to Cursor')).toBeInTheDocument();
});
});
290 changes: 176 additions & 114 deletions static/app/components/events/autofix/autofixRootCause.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,149 @@ function CopyRootCauseButton({
);
}

type CodingAgentIntegration = {
id: string;
name: string;
provider: string;
};

function SolutionActionButton({
cursorIntegrations,
preferredAction,
primaryButtonPriority,
isSelectingRootCause,
isLaunchingAgent,
isLoadingAgents,
submitFindSolution,
handleLaunchCodingAgent,
findSolutionTitle,
}: {
cursorIntegrations: CodingAgentIntegration[];
findSolutionTitle: string;
handleLaunchCodingAgent: (integrationId: string, integrationName: string) => 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:', ''))
: null;

const effectivePreference =
preferredAction === 'seer_solution' || !preferredIntegration
? 'seer_solution'
: preferredAction;

const isSeerPreferred = effectivePreference === 'seer_solution';

// 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;

// If no integrations, show simple Seer button
if (cursorIntegrations.length === 0) {
return (
<Button
size="sm"
priority={primaryButtonPriority}
busy={isSelectingRootCause}
onClick={submitFindSolution}
title={findSolutionTitle}
>
{t('Find Solution')}
</Button>
);
}

const dropdownItems = [
...(isSeerPreferred
? []
: [
{
key: 'seer_solution',
label: t('Find Solution with Seer'),
onAction: submitFindSolution,
disabled: isSelectingRootCause,
},
]),
// Show all integrations except the currently preferred one
...cursorIntegrations
.filter(integration => `cursor:${integration.id}` !== effectivePreference)
.map(integration => ({
key: `cursor:${integration.id}`,
label: (
<Flex gap="md" align="center">
<PluginIcon pluginId="cursor" size={20} />
<div>{t('Send to %s', integration.name)}</div>
{hasDuplicateNames && (
<SmallIntegrationIdText>({integration.id})</SmallIntegrationIdText>
)}
</Flex>
),
onAction: () => handleLaunchCodingAgent(integration.id, integration.name),
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', preferredIntegration!.name);

const primaryButtonProps = isSeerPreferred
? {
onClick: submitFindSolution,
busy: isSelectingRootCause,
icon: undefined,
children: primaryButtonLabel,
}
: {
onClick: () =>
handleLaunchCodingAgent(preferredIntegration!.id, preferredIntegration!.name),
busy: isLaunchingAgent,
icon: <PluginIcon pluginId="cursor" size={16} />,
children: primaryButtonLabel,
};

return (
<ButtonBar merged gap="0">
<Button
size="sm"
priority={primaryButtonPriority}
disabled={isLoadingAgents}
{...primaryButtonProps}
>
{primaryButtonProps.children}
</Button>
<DropdownMenu
items={dropdownItems}
trigger={(triggerProps, isOpen) => (
<DropdownTrigger
{...triggerProps}
size="sm"
priority={primaryButtonPriority}
busy={isSelectingRootCause || isLaunchingAgent}
disabled={isLoadingAgents}
aria-label={t('More solution options')}
icon={
isSelectingRootCause || isLaunchingAgent ? (
<LoadingIndicator size={12} />
) : (
<IconChevron direction={isOpen ? 'up' : 'down'} size="xs" />
)
}
/>
)}
/>
</ButtonBar>
);
}

function AutofixRootCauseDisplay({
causes,
groupId,
Expand Down Expand Up @@ -276,9 +419,11 @@ function AutofixRootCauseDisplay({
runId
);

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

const handleSelectDescription = () => {
if (descriptionRef.current) {
Expand Down Expand Up @@ -323,27 +468,29 @@ function AutofixRootCauseDisplay({
});
};

// Find Cursor integration specifically
const cursorIntegration = codingAgentIntegrations.find(
const cursorIntegrations = codingAgentIntegrations.filter(
integration => integration.provider === 'cursor'
);

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

if (!targetIntegration) {
return;
}

// Save user preference
setPreferredAction('cursor_background_agent');
// Save user preference with specific integration ID
setPreferredAction(`cursor:${integrationId}`);

// Show immediate loading toast
addLoadingMessage(t('Launching %s...', cursorIntegration.name), {duration: 60000});
addLoadingMessage(t('Launching %s...', integrationName), {
duration: 60000,
});

const instruction = solutionText.trim();

launchCodingAgent({
integrationId: cursorIntegration.id,
agentName: cursorIntegration.name,
integrationId: targetIntegration.id,
agentName: targetIntegration.name,
triggerSource: 'root_cause',
instruction: instruction || undefined,
});
Expand Down Expand Up @@ -478,107 +625,17 @@ function AutofixRootCauseDisplay({
/>
<ButtonBar>
<CopyRootCauseButton cause={cause} event={event} />
{cursorIntegration ? (
<ButtonBar merged gap="0">
{preferredAction === 'cursor_background_agent' ? (
<Fragment>
<Button
size="sm"
priority={primaryButtonPriority}
busy={isLaunchingAgent}
disabled={isLoadingAgents}
onClick={handleLaunchCodingAgent}
title={t('Send to Cursor Cloud Agent')}
icon={<PluginIcon pluginId="cursor" size={16} />}
>
{t('Send to Cursor Cloud Agent')}
</Button>
<DropdownMenu
items={[
{
key: 'seer-solution',
label: t('Find Solution with Seer'),
onAction: submitFindSolution,
disabled: isSelectingRootCause,
},
]}
trigger={(triggerProps, isOpen) => (
<DropdownTrigger
{...triggerProps}
size="sm"
priority={primaryButtonPriority}
busy={isSelectingRootCause}
disabled={isLoadingAgents}
aria-label={t('More solution options')}
icon={
isSelectingRootCause ? (
<LoadingIndicator size={12} />
) : (
<IconChevron direction={isOpen ? 'up' : 'down'} size="xs" />
)
}
/>
)}
/>
</Fragment>
) : (
<Fragment>
<Button
size="sm"
priority={primaryButtonPriority}
busy={isSelectingRootCause}
disabled={isLoadingAgents}
onClick={submitFindSolution}
title={findSolutionTitle}
>
{t('Find Solution with Seer')}
</Button>
<DropdownMenu
items={[
{
key: 'cursor-agent',
label: (
<Flex gap="md" align="center">
<PluginIcon pluginId="cursor" size={20} />
<div>{t('Send to Cursor Cloud Agent')}</div>
</Flex>
),
onAction: handleLaunchCodingAgent,
disabled: isLoadingAgents || isLaunchingAgent,
},
]}
trigger={(triggerProps, isOpen) => (
<DropdownTrigger
{...triggerProps}
size="sm"
priority={primaryButtonPriority}
busy={isLaunchingAgent}
disabled={isLoadingAgents}
aria-label={t('More solution options')}
icon={
isLaunchingAgent ? (
<LoadingIndicator size={12} />
) : (
<IconChevron direction={isOpen ? 'up' : 'down'} size="xs" />
)
}
/>
)}
/>
</Fragment>
)}
</ButtonBar>
) : (
<Button
size="sm"
priority={primaryButtonPriority}
busy={isSelectingRootCause}
onClick={submitFindSolution}
title={findSolutionTitle}
>
{t('Find Solution')}
</Button>
)}
<SolutionActionButton
cursorIntegrations={cursorIntegrations}
preferredAction={preferredAction}
primaryButtonPriority={primaryButtonPriority}
isSelectingRootCause={isSelectingRootCause}
isLaunchingAgent={isLaunchingAgent}
isLoadingAgents={isLoadingAgents}
submitFindSolution={submitFindSolution}
handleLaunchCodingAgent={handleLaunchCodingAgent}
findSolutionTitle={findSolutionTitle}
/>
</ButtonBar>
{status === AutofixStatus.COMPLETED && (
<AutofixStepFeedback stepType="root_cause" groupId={groupId} runId={runId} />
Expand Down Expand Up @@ -697,3 +754,8 @@ const DropdownTrigger = styled(Button)`
border-radius: 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0;
border-left: none;
`;

const SmallIntegrationIdText = styled('div')`
font-size: ${p => p.theme.fontSize.sm};
color: ${p => p.theme.subText};
`;
Loading
Loading