Skip to content
Open
114 changes: 73 additions & 41 deletions static/app/components/agentHandoff/agentHandoffPrompt.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {useState, type PointerEventHandler} from 'react';
import {ThemeProvider} from '@emotion/react';
import {css, ThemeProvider, type Theme} from '@emotion/react';
import styled from '@emotion/styled';
import {IconCopy} from '@sentry/icons/copy';
import {IconOpen} from '@sentry/icons/open';
import {IconSettings} from '@sentry/icons/settings';
import {useStore} from 'zustand';

Expand Down Expand Up @@ -84,55 +85,64 @@ export function AgentHandoffPrompt<Flow extends AgentHandoffFlow>({
<Stack gap="md">
<ThemeProvider theme={darkTheme}>
<Container
background="secondary"
background="primary"
border="primary"
radius="md"
padding="lg"
padding="md xl"
paddingRight="3xl"
position="relative"
onCopy={onCopy}
>
<Text
ref={promptRef}
as="div"
monospace
size="sm"
tabIndex={tabIndex}
textWrap="wrap"
onPointerDown={handlePointerDown}
>
{tct(
'Follow the instructions verbatim at [guideUrl]. Your handoff code is [token].',
{
guideUrl,
token: (
<MaskedCode
monospace
variant="warning"
title={t('The secret code authorizes your agent on your behalf')}
>
sntryh_****
</MaskedCode>
),
}
)}
</Text>
<Container position="absolute" top="8px" right="8px">
<Button
aria-label={t('Copy agent prompt')}
busy={handoff.isPending}
disabled={copyDisabled}
icon={<IconCopy />}
size="zero"
variant="transparent"
onClick={copyPrompt}
/>
</Container>
{promptBoxProps => (
<PromptBox {...promptBoxProps} onCopy={onCopy}>
<PromptText
ref={promptRef}
as="div"
monospace
size="sm"
tabIndex={tabIndex}
textWrap="wrap"
onPointerDown={handlePointerDown}
>
{tct(
'Follow the instructions verbatim at [guideUrl]. Your handoff code is [token].',
{
guideUrl,
token: (
<MaskedCode
monospace
variant="warning"
title={t('The secret code authorizes your agent on your behalf')}
>
sntryh_****
</MaskedCode>
),
}
)}
</PromptText>
<Container position="absolute" top="8px" right="8px">
<Button
aria-label={t('Copy agent prompt')}
busy={handoff.isPending}
disabled={copyDisabled}
icon={<IconCopy />}
size="zero"
variant="transparent"
onClick={copyPrompt}
/>
</Container>
</PromptBox>
)}
</Container>
</ThemeProvider>

<Flex align="center" justify="between" gap="lg">
<Text size="sm">
<ExternalLink href={previewUrl}>{t('See the full instructions')}</ExternalLink>
<ExternalLink href={previewUrl}>
<Flex as="span" align="center" gap="xs">
{t('See the full instructions')}
<IconOpen size="xs" />
</Flex>
</ExternalLink>
</Text>

<Flex align="center" gap="sm">
Expand Down Expand Up @@ -192,9 +202,31 @@ export function AgentHandoffPrompt<Flow extends AgentHandoffFlow>({
);
}

const PromptBox = styled('div')`
box-shadow: inset 0 1px 0
${p => p.theme.tokens.interactive.chonky.debossed.neutral.chonk};
`;

const accentSelection = (p: {theme: Theme}) => css`
&::selection {
background: ${p.theme.tokens.background.accent.vibrant};
color: ${p.theme.tokens.content.onVibrant.light};
}
`;

const PromptText = styled(Text)`
${accentSelection}

/* Focus is only set programmatically to select the prompt; it is not tabbable. */
&:focus {
outline: none;
}
`;

const MaskedCode = styled(InfoText)`
background: ${p => p.theme.tokens.background.transparent.warning.muted};
border-radius: ${p => p.theme.radius.sm};
padding: 0 ${p => p.theme.space['2xs']};
text-decoration: none;
${accentSelection}
`;
48 changes: 19 additions & 29 deletions static/app/components/onboarding/agenticProgress/agentSetupCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ReactNode} from 'react';
import {Fragment, type ReactNode} from 'react';
import {IconCheckmark} from '@sentry/icons/checkmark';

import {Tag} from '@sentry/scraps/badge';
Expand All @@ -7,7 +7,7 @@ import {Heading, Text} from '@sentry/scraps/text';

import {AgentHandoffPrompt} from 'sentry/components/agentHandoff/agentHandoffPrompt';
import {AgentHandoffFlow} from 'sentry/components/agentHandoff/useAgentHandoff';
import {t, tct} from 'sentry/locale';
import {t} from 'sentry/locale';

import type {AgenticProgressRun} from './types';

Expand All @@ -29,43 +29,33 @@ export function AgentSetupCard({
run,
}: AgentSetupCardProps) {
return (
<Stack gap="0" background="primary" border="primary" radius="xl" padding="xl">
<Stack gap="xs">
<Flex align="center" gap="sm" wrap="wrap">
<Fragment>
<Stack gap="2xs">
<Flex align="center" justify="between" gap="md" wrap="wrap">
<Heading as="h3" size="lg">
{t('Let your agent set up Sentry')}
</Heading>
<Tag variant="info">{t('Recommended')}</Tag>
</Flex>
<Text as="p" size="md" variant="secondary">
{t('Paste one prompt and your agent does the rest.')}
<Text as="p" size="lg" variant="secondary">
{t('Paste one prompt and your agent does the rest')}
</Text>
</Stack>

<Stack as="ul" role="list" gap="md" padding="xl 0">
<Benefit>{tct('[emphasis:Detects your stack]', {emphasis: <strong />})}</Benefit>
<Benefit>
{tct('[emphasis:Installs and configures the SDKs] in your code', {
emphasis: <strong />,
})}
</Benefit>
<Benefit>
{tct('[emphasis:Sends a test error] and confirms it, so you know it works', {
emphasis: <strong />,
})}
</Benefit>
<Stack as="ul" role="list" gap="sm" padding="xl 0" margin="0">
<Benefit>{t('Detects your stack and installs the Sentry CLI')}</Benefit>
<Benefit>{t('Installs and configures the right SDKs')}</Benefit>
<Benefit>{t('Sends a test error to confirm it works')}</Benefit>
</Stack>

<Stack>
<AgentHandoffPrompt
context={run ? {runId: run.runId} : null}
feedbackSource={feedbackSource}
flow={flow}
onCopySuccess={() => onCopyCommand('prompt')}
onSelect={() => onSelectSnippet('prompt')}
/>
</Stack>
</Stack>
<AgentHandoffPrompt
context={run ? {runId: run.runId} : null}
feedbackSource={feedbackSource}
flow={flow}
onCopySuccess={() => onCopyCommand('prompt')}
onSelect={() => onSelectSnippet('prompt')}
/>
</Fragment>
);
}

Expand Down
87 changes: 31 additions & 56 deletions static/app/components/onboarding/agenticProgress/agenticSetup.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import {IconChevron} from '@sentry/icons/chevron';
import {IconSliders} from '@sentry/icons/sliders';
import {AnimatePresence, motion} from 'framer-motion';

import {Alert} from '@sentry/scraps/alert';
import {Button} from '@sentry/scraps/button';
import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout';
import {Container, Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';
import {ToggleCard} from '@sentry/scraps/toggleCard';

import {AgentHandoffFlow} from 'sentry/components/agentHandoff/useAgentHandoff';
import {
SETUP_CARD_ICON_SIZE,
SETUP_CARD_MARKER_PX,
} from 'sentry/components/onboarding/consts';
import {ScmCollapsibleReveal} from 'sentry/components/onboarding/scm/scmCollapsibleReveal';
import {t} from 'sentry/locale';
import {t, tct} from 'sentry/locale';

import {AgenticProgress} from './agenticProgressList';
import {AgentSetupCard, type AgentSetupCopySource} from './agentSetupCard';
import {SetupCardFrame} from './setupCardFrame';
import type {AgenticProgressRun} from './types';

const MotionContainer = motion.create(Container);
Expand All @@ -37,6 +32,8 @@ interface AgenticSetupProps {
onSetupInBrowser: () => void;
run: AgenticProgressRun | undefined;
handoffFlow?: AgentHandoffFlow;
/** Tilts the setup card and adds the stacked brand border and shadows. */
layered?: boolean;
onboardingCode?: string;
}

Expand All @@ -54,6 +51,7 @@ export function AgenticSetup({
onRefresh,
run,
handoffFlow = AgentHandoffFlow.ONBOARDING,
layered = false,
}: AgenticSetupProps) {
const hasRunFailed = run?.runStatus === 'failed' || run?.runStatus === 'cancelled';
const showsProgress =
Expand Down Expand Up @@ -124,13 +122,15 @@ export function AgenticSetup({
exit={{opacity: 0}}
transition={CARD_MORPH_TRANSITION}
>
<AgentSetupCard
feedbackSource={feedbackSource}
flow={handoffFlow}
onCopyCommand={onCopyCommand}
onSelectSnippet={onSelectSnippet}
run={run}
/>
<SetupCardFrame layered={layered}>
<AgentSetupCard
feedbackSource={feedbackSource}
flow={handoffFlow}
onCopyCommand={onCopyCommand}
onSelectSnippet={onSelectSnippet}
run={run}
/>
</SetupCardFrame>
</MotionContainer>
)}
</AnimatePresence>
Expand All @@ -143,50 +143,25 @@ export function AgenticSetup({
</ScmCollapsibleReveal>

<ScmCollapsibleReveal open={!showsProgress || hasRunFailed}>
<Stack gap="2xl" align="center" width="100%">
<Text variant="muted" size="md" bold>
{t('or')}
</Text>

<ToggleCard
variant="button"
onClick={onSetupInBrowser}
data-test-id="onboarding-setup-in-browser"
>
<Grid
columns={`${SETUP_CARD_MARKER_PX} 1fr max-content`}
gap="0 md"
width="100%"
areas={`
"icon title chevron"
". body body"
`}
<Container width="100%">
{buttonProps => (
<Button
{...buttonProps}
variant="transparent"
onClick={onSetupInBrowser}
data-test-id="onboarding-setup-in-browser"
>
<Flex area="icon" align="center" justify="center">
<IconSliders size={SETUP_CARD_ICON_SIZE} variant="secondary" />
</Flex>
<Flex area="title" align="center">
<Text bold size="lg">
{t('Set up manually')}
<Flex as="span" align="center" gap="sm">
<Text variant="secondary">
{tct('Prefer to do it yourself? [manual]', {
manual: <Text variant="accent">{t('Set up manually')}</Text>,
})}
</Text>
<IconChevron direction="right" size="sm" variant="accent" />
</Flex>
<Flex area="chevron" align="center">
<IconChevron
direction="right"
size={SETUP_CARD_ICON_SIZE}
variant="secondary"
/>
</Flex>
<Container area="body" paddingTop="xs">
<Text variant="muted" size="md" density="comfortable" textWrap="pretty">
{t(
'Connect a repo, choose what to instrument and where alerts land, then follow the instructions for your project'
)}
</Text>
</Container>
</Grid>
</ToggleCard>
</Stack>
</Button>
)}
</Container>
</ScmCollapsibleReveal>
</Stack>
);
Expand Down
Loading
Loading