Skip to content

Commit 3424a99

Browse files
feat(seer): Gate autofix overview handoff on precomputed repo eligibility (#122229)
## Summary Fixes the autofix overview dropdown flash: on first open of a run's "More Seer options" menu, it briefly showed only "Open Seer" and then popped the coding-agent handoff options in a moment later, once a per-card, fully-paginated Seer repos fetch drained. ## Changes - Read the new `hasReposConnected` / `hasNonGithubRepo` booleans off the overview payload (`issue.project`, present under `expand=scmInfo`) to gate handoff options, instead of fetching the project's Seer repos per card. - `useCodingAgents` accepts an optional `repoEligibility`; when provided it skips the repos query entirely and derives the disabled-reason from the precomputed flags. When absent, it falls back to the repos query — so the change degrades gracefully before the backend fields exist. - The dropdown now shows a single loading state until its options are ready and reveals them together, instead of rendering the static "Open Seer" item first and staggering the agents in after. ## Depends on Backend PR #122226 (adds the `hasReposConnected` / `hasNonGithubRepo` fields). Frontend/backend are not atomically deployed; that PR should land first. This PR is safe to deploy independently because it falls back to the existing repos query when the fields are absent.
1 parent 86cb059 commit 3424a99

4 files changed

Lines changed: 162 additions & 17 deletions

File tree

static/app/components/events/autofix/v3/useCodingAgents.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import {
1717
import {useOrganization} from 'sentry/utils/useOrganization';
1818
import type {SeerExplorerRunId} from 'sentry/views/seerExplorer/types';
1919

20+
interface RepoEligibility {
21+
hasNonGithubRepo: boolean;
22+
hasReposConnected: boolean;
23+
}
24+
2025
interface UseCodingAgentsOptions {
2126
autofix: Pick<ReturnType<typeof useExplorerAutofix>, 'triggerCodingAgentHandoff'>;
2227
group: {id: string; project: AvatarProject};
@@ -25,6 +30,7 @@ interface UseCodingAgentsOptions {
2530
step: 'root_cause' | 'solution' | 'code_changes';
2631
enabled?: boolean;
2732
onHandoff?: () => void;
33+
repoEligibility?: RepoEligibility;
2834
}
2935

3036
export function useCodingAgents({
@@ -35,34 +41,43 @@ export function useCodingAgents({
3541
referrer,
3642
enabled = true,
3743
onHandoff,
44+
repoEligibility,
3845
}: UseCodingAgentsOptions) {
3946
const organization = useOrganization();
4047
const {triggerCodingAgentHandoff} = autofix;
4148

42-
const {data: codingAgentResponse} = useQuery({
49+
const {data: codingAgentResponse, isLoading: isAgentsLoading} = useQuery({
4350
...organizationIntegrationsCodingAgents(organization),
4451
enabled,
4552
});
4653

54+
const reposEnabled = enabled && repoEligibility === undefined;
4755
const reposQuery = useInfiniteQuery({
4856
...getSeerProjectReposInfiniteQueryOptions({organization, project: group.project}),
49-
enabled,
57+
enabled: reposEnabled,
5058
select: ({pages}) => pages.flatMap(page => page.json),
5159
});
52-
useFetchAllPages({result: reposQuery, enabled});
60+
useFetchAllPages({result: reposQuery, enabled: reposEnabled});
5361
const repos = reposQuery.data ?? [];
5462

5563
// Wait until pagination is fully drained so the gate is computed over every repo.
5664
const isReposLoading =
57-
reposQuery.isPending || reposQuery.isFetchingNextPage || reposQuery.hasNextPage;
58-
const hasNoRepos = repos.length === 0;
59-
const hasNonGithubRepo = repos.some(repo => !isGitHubProvider(repo.provider));
65+
repoEligibility === undefined &&
66+
(reposQuery.isPending || reposQuery.isFetchingNextPage || reposQuery.hasNextPage);
67+
const hasNoRepos = repoEligibility
68+
? !repoEligibility.hasReposConnected
69+
: repos.length === 0;
70+
const hasNonGithubRepo = repoEligibility
71+
? repoEligibility.hasNonGithubRepo
72+
: repos.some(repo => !isGitHubProvider(repo.provider));
6073

6174
const codingAgentIntegrations = useMemo(
6275
() => (isReposLoading ? undefined : codingAgentResponse?.integrations),
6376
[codingAgentResponse?.integrations, isReposLoading]
6477
);
6578

79+
const isLoading = enabled && (isAgentsLoading || isReposLoading);
80+
6681
const codingAgentDisabledReason = hasNoRepos
6782
? t('Connect a GitHub repository to hand off to a coding agent.')
6883
: hasNonGithubRepo
@@ -91,5 +106,10 @@ export function useCodingAgents({
91106
[triggerCodingAgentHandoff, organization, runId, group, step, referrer, onHandoff]
92107
);
93108

94-
return {codingAgentIntegrations, codingAgentDisabledReason, handleCodingAgentHandoff};
109+
return {
110+
codingAgentIntegrations,
111+
codingAgentDisabledReason,
112+
handleCodingAgentHandoff,
113+
isLoading,
114+
};
95115
}

static/app/views/seerWorkflows/overview/overviewCardAction.spec.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,4 +290,96 @@ describe('OverviewCardAction', () => {
290290
});
291291
expect(agentItem).toHaveAttribute('aria-disabled', 'true');
292292
});
293+
294+
function runWithEligibility(hasReposConnected: boolean, hasNonGithubRepo: boolean) {
295+
return runFixture({
296+
issue: {
297+
...issueFixture(),
298+
project: {
299+
id: '2',
300+
slug: 'project-slug',
301+
platform: 'python',
302+
hasReposConnected,
303+
hasNonGithubRepo,
304+
},
305+
},
306+
});
307+
}
308+
309+
it('uses precomputed repo eligibility and skips the repos fetch', async () => {
310+
const reposRequest = MockApiClient.addMockResponse({
311+
url: '/projects/org-slug/project-slug/seer/repos/',
312+
body: [{provider: 'github'}],
313+
});
314+
315+
render(
316+
<OverviewCardAction
317+
run={runWithEligibility(true, false)}
318+
sectionKey="needs_investigation"
319+
/>,
320+
{organization}
321+
);
322+
323+
await userEvent.click(screen.getByRole('button', {name: 'More Seer options'}));
324+
325+
const agentItem = await screen.findByRole('menuitemradio', {
326+
name: 'Send to Claude Agent',
327+
});
328+
expect(agentItem).not.toHaveAttribute('aria-disabled', 'true');
329+
expect(reposRequest).not.toHaveBeenCalled();
330+
});
331+
332+
it('disables handoff from precomputed eligibility without fetching repos', async () => {
333+
const reposRequest = MockApiClient.addMockResponse({
334+
url: '/projects/org-slug/project-slug/seer/repos/',
335+
body: [{provider: 'github'}],
336+
});
337+
338+
render(
339+
<OverviewCardAction
340+
run={runWithEligibility(false, false)}
341+
sectionKey="needs_investigation"
342+
/>,
343+
{organization}
344+
);
345+
346+
await userEvent.click(screen.getByRole('button', {name: 'More Seer options'}));
347+
348+
const agentItem = await screen.findByRole('menuitemradio', {
349+
name: 'Send to Claude Agent',
350+
});
351+
expect(agentItem).toHaveAttribute('aria-disabled', 'true');
352+
expect(reposRequest).not.toHaveBeenCalled();
353+
});
354+
355+
it('shows a loading state before revealing all options at once', async () => {
356+
MockApiClient.addMockResponse({
357+
url: '/organizations/org-slug/integrations/coding-agents/',
358+
body: {
359+
integrations: [{id: '123', name: 'Claude Agent', provider: 'claude_code'}],
360+
},
361+
asyncDelay: 50,
362+
});
363+
364+
render(
365+
<OverviewCardAction
366+
run={runWithEligibility(true, false)}
367+
sectionKey="needs_investigation"
368+
/>,
369+
{organization}
370+
);
371+
372+
await userEvent.click(screen.getByRole('button', {name: 'More Seer options'}));
373+
374+
expect(
375+
screen.queryByRole('menuitemradio', {name: 'Open Seer'})
376+
).not.toBeInTheDocument();
377+
378+
expect(
379+
await screen.findByRole('menuitemradio', {name: 'Open Seer'})
380+
).toBeInTheDocument();
381+
expect(
382+
screen.getByRole('menuitemradio', {name: 'Send to Claude Agent'})
383+
).toBeInTheDocument();
384+
});
293385
});

static/app/views/seerWorkflows/overview/overviewCardAction.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,26 @@ export function OverviewCardAction({
109109
}
110110
);
111111

112-
const {codingAgentIntegrations, codingAgentDisabledReason, handleCodingAgentHandoff} =
113-
useCodingAgents({
114-
autofix,
115-
group: {id: run.groupId, project: run.issue.project},
116-
runId: run.seerRunId,
117-
step: config.handoffStep,
118-
referrer: 'autofix-overview',
119-
enabled: menuOpened,
120-
});
112+
const {hasReposConnected, hasNonGithubRepo} = run.issue.project;
113+
const repoEligibility =
114+
hasReposConnected === undefined
115+
? undefined
116+
: {hasReposConnected, hasNonGithubRepo: hasNonGithubRepo ?? false};
117+
118+
const {
119+
codingAgentIntegrations,
120+
codingAgentDisabledReason,
121+
handleCodingAgentHandoff,
122+
isLoading: isLoadingOptions,
123+
} = useCodingAgents({
124+
autofix,
125+
group: {id: run.groupId, project: run.issue.project},
126+
runId: run.seerRunId,
127+
step: config.handoffStep,
128+
referrer: 'autofix-overview',
129+
enabled: menuOpened,
130+
repoEligibility,
131+
});
121132

122133
const isDraftPr = sectionKey === 'code_changes_ready';
123134
const {permissionsTarget, isPending: isCreatePrGatePending} = useAutofixCreatePrGate({
@@ -126,6 +137,21 @@ export function OverviewCardAction({
126137
});
127138

128139
const menuItems = useMemo<MenuItemProps[]>(() => {
140+
if (isLoadingOptions) {
141+
return [
142+
{
143+
key: 'loading',
144+
textValue: t('Loading'),
145+
disabled: true,
146+
label: (
147+
<Flex justify="center" padding="sm">
148+
<LoadingIndicator mini size={20} />
149+
</Flex>
150+
),
151+
},
152+
];
153+
}
154+
129155
const agentItems = (codingAgentIntegrations ?? []).map(integration => {
130156
const actionLabel =
131157
integration.requires_identity && !integration.has_identity
@@ -170,6 +196,7 @@ export function OverviewCardAction({
170196
...agentItems,
171197
];
172198
}, [
199+
isLoadingOptions,
173200
codingAgentIntegrations,
174201
codingAgentDisabledReason,
175202
handleCodingAgentHandoff,

static/app/views/seerWorkflows/overview/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,13 @@ export interface OverviewRunIssue {
184184
owners: SuggestedOwner[];
185185
priority: PriorityLevel | null;
186186
priorityLockedAt: string | null;
187-
project: {id: string; slug: string; platform?: PlatformKey};
187+
project: {
188+
id: string;
189+
slug: string;
190+
hasNonGithubRepo?: boolean;
191+
hasReposConnected?: boolean;
192+
platform?: PlatformKey;
193+
};
188194
substatus: string | null;
189195
userCount: number | null;
190196
}

0 commit comments

Comments
 (0)