Skip to content

Commit bd71870

Browse files
authored
feat(seer-cursor): add cursor integration to onboarding flow (#103156)
Makes the cursor agent integration more discoverable by adding another seer onboarding step. The main difference between this step and other seer steps is that it will not keep the onboarding steps around if you skip it (because not everyone has the cursor integration, so we make it truly skippable and not naggy) Without integration set up: <img width="1800" height="518" alt="image" src="https://github.com/user-attachments/assets/7ca2f19a-739d-4851-a438-952fc0401221" /> With integration set up but without handoff set: <img width="2040" height="526" alt="image" src="https://github.com/user-attachments/assets/d260cc90-4179-45eb-8a39-59c7d46bf82a" /> With handoff setup or onboarding step skipped: <img width="1066" height="236" alt="image" src="https://github.com/user-attachments/assets/582e4017-cdcd-4f36-8d96-ecaccb282e49" />
1 parent 5136da9 commit bd71870

File tree

3 files changed

+543
-6
lines changed

3 files changed

+543
-6
lines changed

static/app/views/issueDetails/streamline/sidebar/seerDrawer.spec.tsx

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ describe('SeerDrawer', () => {
103103

104104
beforeEach(() => {
105105
MockApiClient.clearMockResponses();
106+
localStorage.clear();
106107

107108
MockApiClient.addMockResponse({
108109
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/setup/`,
@@ -136,12 +137,26 @@ describe('SeerDrawer', () => {
136137
url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`,
137138
body: [],
138139
});
140+
MockApiClient.addMockResponse({
141+
url: `/organizations/${mockProject.organization.slug}/group-search-views/`,
142+
body: [],
143+
});
139144
MockApiClient.addMockResponse({
140145
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/`,
141146
body: {
142147
autofixAutomationTuning: 'off',
143148
},
144149
});
150+
MockApiClient.addMockResponse({
151+
url: `/organizations/${mockProject.organization.slug}/integrations/coding-agents/`,
152+
body: {
153+
integrations: [],
154+
},
155+
});
156+
MockApiClient.addMockResponse({
157+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/autofix-repos/`,
158+
body: [],
159+
});
145160
});
146161

147162
it('renders consent state if not consented', async () => {
@@ -633,4 +648,161 @@ describe('SeerDrawer', () => {
633648
screen.getByText(/It currently only supports GitHub repositories/)
634649
).toBeInTheDocument();
635650
});
651+
652+
it('shows cursor integration onboarding step if integration is installed but handoff not configured', async () => {
653+
MockApiClient.addMockResponse({
654+
url: `/organizations/${mockProject.organization.slug}/integrations/coding-agents/`,
655+
body: {
656+
integrations: [
657+
{
658+
id: '123',
659+
provider: 'cursor',
660+
name: 'Cursor',
661+
},
662+
],
663+
},
664+
});
665+
MockApiClient.addMockResponse({
666+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/seer/preferences/`,
667+
body: {
668+
code_mapping_repos: [],
669+
preference: {
670+
repositories: [{external_id: 'repo-123', name: 'org/repo', provider: 'github'}],
671+
automated_run_stopping_point: 'root_cause',
672+
// No automation_handoff
673+
},
674+
},
675+
});
676+
MockApiClient.addMockResponse({
677+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/`,
678+
body: {
679+
autofixAutomationTuning: 'medium',
680+
seerScannerAutomation: true,
681+
},
682+
});
683+
MockApiClient.addMockResponse({
684+
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`,
685+
body: {autofix: null},
686+
});
687+
MockApiClient.addMockResponse({
688+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/autofix-repos/`,
689+
body: [
690+
{
691+
name: 'org/repo',
692+
provider: 'github',
693+
owner: 'org',
694+
external_id: 'repo-123',
695+
is_readable: true,
696+
is_writeable: true,
697+
},
698+
],
699+
});
700+
MockApiClient.addMockResponse({
701+
url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`,
702+
body: [
703+
{
704+
id: '1',
705+
name: 'Fixability View',
706+
query: 'is:unresolved issue.seer_actionability:high',
707+
starred: true,
708+
},
709+
],
710+
});
711+
712+
render(<SeerDrawer event={mockEvent} group={mockGroup} project={mockProject} />, {
713+
organization: OrganizationFixture({
714+
features: ['gen-ai-features', 'integrations-cursor', 'issue-views'],
715+
}),
716+
});
717+
718+
await waitForElementToBeRemoved(() =>
719+
screen.queryByTestId('ai-setup-loading-indicator')
720+
);
721+
722+
expect(
723+
await screen.findByText('Hand Off to Cursor Background Agents')
724+
).toBeInTheDocument();
725+
expect(
726+
screen.getByRole('button', {name: 'Set Seer to hand off to Cursor'})
727+
).toBeInTheDocument();
728+
});
729+
730+
it('does not show cursor integration step if localStorage skip key is set', async () => {
731+
// Set skip key BEFORE rendering
732+
localStorage.setItem(`seer-onboarding-cursor-skipped:${mockProject.id}`, 'true');
733+
734+
MockApiClient.addMockResponse({
735+
url: `/organizations/${mockProject.organization.slug}/integrations/coding-agents/`,
736+
body: {
737+
integrations: [
738+
{
739+
id: '123',
740+
provider: 'cursor',
741+
name: 'Cursor',
742+
},
743+
],
744+
},
745+
});
746+
MockApiClient.addMockResponse({
747+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/seer/preferences/`,
748+
body: {
749+
code_mapping_repos: [],
750+
preference: {
751+
repositories: [{external_id: 'repo-123', name: 'org/repo', provider: 'github'}],
752+
automated_run_stopping_point: 'root_cause',
753+
// No automation_handoff
754+
},
755+
},
756+
});
757+
MockApiClient.addMockResponse({
758+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/`,
759+
body: {
760+
autofixAutomationTuning: 'medium',
761+
seerScannerAutomation: true,
762+
},
763+
});
764+
MockApiClient.addMockResponse({
765+
url: `/organizations/${mockProject.organization.slug}/issues/${mockGroup.id}/autofix/`,
766+
body: {autofix: null},
767+
});
768+
MockApiClient.addMockResponse({
769+
url: `/projects/${mockProject.organization.slug}/${mockProject.slug}/autofix-repos/`,
770+
body: [
771+
{
772+
name: 'org/repo',
773+
provider: 'github',
774+
owner: 'org',
775+
external_id: 'repo-123',
776+
is_readable: true,
777+
is_writeable: true,
778+
},
779+
],
780+
});
781+
MockApiClient.addMockResponse({
782+
url: `/organizations/${mockProject.organization.slug}/group-search-views/starred/`,
783+
body: [
784+
{
785+
id: '1',
786+
name: 'Fixability View',
787+
query: 'is:unresolved issue.seer_actionability:high',
788+
starred: true,
789+
},
790+
],
791+
});
792+
793+
render(<SeerDrawer event={mockEvent} group={mockGroup} project={mockProject} />, {
794+
organization: OrganizationFixture({
795+
features: ['gen-ai-features', 'integrations-cursor', 'issue-views'],
796+
}),
797+
});
798+
799+
await waitForElementToBeRemoved(() =>
800+
screen.queryByTestId('ai-setup-loading-indicator')
801+
);
802+
803+
// Should not show the step since it was skipped
804+
expect(
805+
screen.queryByText('Hand Off to Cursor Background Agents')
806+
).not.toBeInTheDocument();
807+
});
636808
});

static/app/views/issueDetails/streamline/sidebar/seerNotices.spec.tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ describe('SeerNotices', () => {
4949
url: `/projects/${organization.slug}/${ProjectFixture().slug}/autofix-repos/`,
5050
body: [createRepository()],
5151
});
52+
MockApiClient.addMockResponse({
53+
url: `/organizations/${organization.slug}/integrations/coding-agents/`,
54+
body: {
55+
integrations: [],
56+
},
57+
});
5258
});
5359

5460
it('shows automation step if automation is allowed and tuning is off', async () => {
@@ -98,6 +104,167 @@ describe('SeerNotices', () => {
98104
});
99105
});
100106

107+
it('shows cursor integration step if integration is installed but handoff not configured', async () => {
108+
MockApiClient.addMockResponse({
109+
url: `/organizations/${organization.slug}/integrations/coding-agents/`,
110+
body: {
111+
integrations: [
112+
{
113+
id: '123',
114+
provider: 'cursor',
115+
name: 'Cursor',
116+
},
117+
],
118+
},
119+
});
120+
MockApiClient.addMockResponse({
121+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/seer/preferences/`,
122+
body: {
123+
code_mapping_repos: [],
124+
preference: {
125+
repositories: [],
126+
automated_run_stopping_point: 'root_cause',
127+
// No automation_handoff - handoff is not configured
128+
},
129+
},
130+
});
131+
MockApiClient.addMockResponse({
132+
method: 'GET',
133+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/`,
134+
body: {
135+
autofixAutomationTuning: 'medium',
136+
},
137+
});
138+
MockApiClient.addMockResponse({
139+
url: `/organizations/${organization.slug}/group-search-views/starred/`,
140+
body: [
141+
GroupSearchViewFixture({
142+
query: 'is:unresolved issue.seer_actionability:high',
143+
starred: true,
144+
}),
145+
],
146+
});
147+
const project = getProjectWithAutomation('medium');
148+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
149+
organization: {
150+
...organization,
151+
features: ['integrations-cursor'],
152+
},
153+
});
154+
await waitFor(() => {
155+
expect(
156+
screen.getByText('Hand Off to Cursor Background Agents')
157+
).toBeInTheDocument();
158+
});
159+
});
160+
161+
it('does not show cursor integration step if localStorage skip key is set', () => {
162+
// Set localStorage skip key
163+
localStorage.setItem(`seer-onboarding-cursor-skipped:${ProjectFixture().id}`, 'true');
164+
165+
MockApiClient.addMockResponse({
166+
url: `/organizations/${organization.slug}/integrations/coding-agents/`,
167+
body: {
168+
integrations: [
169+
{
170+
id: '123',
171+
provider: 'cursor',
172+
name: 'Cursor',
173+
},
174+
],
175+
},
176+
});
177+
MockApiClient.addMockResponse({
178+
method: 'GET',
179+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/`,
180+
body: {
181+
autofixAutomationTuning: 'medium',
182+
},
183+
});
184+
MockApiClient.addMockResponse({
185+
url: `/organizations/${organization.slug}/group-search-views/starred/`,
186+
body: [
187+
GroupSearchViewFixture({
188+
query: 'is:unresolved issue.seer_actionability:high',
189+
starred: true,
190+
}),
191+
],
192+
});
193+
const project = getProjectWithAutomation('medium');
194+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
195+
organization: {
196+
...organization,
197+
features: ['integrations-cursor'],
198+
},
199+
});
200+
201+
// Should not show the cursor step since it was skipped
202+
expect(
203+
screen.queryByText('Hand Off to Cursor Background Agents')
204+
).not.toBeInTheDocument();
205+
206+
// Clean up localStorage
207+
localStorage.removeItem(`seer-onboarding-cursor-skipped:${ProjectFixture().id}`);
208+
});
209+
210+
it('does not show cursor integration step if handoff is already configured', () => {
211+
MockApiClient.addMockResponse({
212+
url: `/organizations/${organization.slug}/integrations/coding-agents/`,
213+
body: {
214+
integrations: [
215+
{
216+
id: '123',
217+
provider: 'cursor',
218+
name: 'Cursor',
219+
},
220+
],
221+
},
222+
});
223+
MockApiClient.addMockResponse({
224+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/seer/preferences/`,
225+
body: {
226+
code_mapping_repos: [],
227+
preference: {
228+
repositories: [],
229+
automated_run_stopping_point: 'root_cause',
230+
automation_handoff: {
231+
handoff_point: 'root_cause',
232+
target: 'cursor_background_agent',
233+
integration_id: 123,
234+
},
235+
},
236+
},
237+
});
238+
MockApiClient.addMockResponse({
239+
method: 'GET',
240+
url: `/projects/${organization.slug}/${ProjectFixture().slug}/`,
241+
body: {
242+
autofixAutomationTuning: 'medium',
243+
},
244+
});
245+
MockApiClient.addMockResponse({
246+
url: `/organizations/${organization.slug}/group-search-views/starred/`,
247+
body: [
248+
GroupSearchViewFixture({
249+
query: 'is:unresolved issue.seer_actionability:high',
250+
starred: true,
251+
}),
252+
],
253+
});
254+
const project = getProjectWithAutomation('medium');
255+
render(<SeerNotices groupId="123" hasGithubIntegration project={project} />, {
256+
organization: {
257+
...organization,
258+
features: ['integrations-cursor'],
259+
},
260+
});
261+
262+
// Should not show the cursor step since handoff is already configured
263+
expect(
264+
screen.queryByText('Hand Off to Cursor Background Agents')
265+
).not.toBeInTheDocument();
266+
});
267+
101268
it('does not render guided steps if all onboarding steps are complete', () => {
102269
MockApiClient.addMockResponse({
103270
method: 'GET',
@@ -126,5 +293,8 @@ describe('SeerNotices', () => {
126293
expect(screen.queryByText('Pick Repositories to Work In')).not.toBeInTheDocument();
127294
expect(screen.queryByText('Unleash Automation')).not.toBeInTheDocument();
128295
expect(screen.queryByText('Get Some Quick Wins')).not.toBeInTheDocument();
296+
expect(
297+
screen.queryByText('Hand Off to Cursor Background Agents')
298+
).not.toBeInTheDocument();
129299
});
130300
});

0 commit comments

Comments
 (0)