feat(seer): Autofix overview sections driven by issue.autofix_state#120294
Conversation
d534132 to
de53999
Compare
📊 Type Coverage Diff
🔍 3 new type safety issues introducedNon-null assertions (
Type assertions (
This is informational only and does not block the PR. |
de53999 to
a0ac41e
Compare
Squash of feat/autofix-overview-state-sections. Rebuild the Seer autofix overview cards around a single narrative flow (progress ring, proposed fix / diagnosis body, next steps, inline file differ, deep-link focus mode, project selector, table view toggle, Investigate fallback CTA) and drive the overview sections from issue.autofix_state activity data.
The overview only ever renders the five AutofixStateKey status groups, so the awaiting_input/errored/running status-group metadata, their tooltip description path, and the widened StatusGroupKey union were unreachable. Drop them along with the unused ATTENTION_REASONS export, the write-only OverviewRow.lastSeen field, and the never-read culprit/seerFixabilityScore fields on OverviewIssue.
Sections carried isError/refetch that nothing rendered; a failed bucket now shows a retryable LoadingError inline while its siblings keep loading. The period filter's 'All time' option was a lie: statsPeriod fell back to '90d' either way. Replace it with an explicit 'Last 90 days' default and thread the active period through the row so the count tooltip states the real window instead of a hardcoded 90 days. lastActivityAt now takes the most recent of its candidate timestamps rather than the first defined one, and the diff-size pill is focusable so its churn tooltip is reachable by keyboard and assistive tech.
Trim stale and overwrought comments across the overview (references to a removed progress ring, a nonexistent Diagnosis block, and running/errored groups that no longer exist), and move the buildAnalysis docstring onto the function it describes. Rename the client-internal analysis key reviewer_notes -> next_steps and the AutofixRunStatus bucket COMPLETED -> SETTLED to match what the UI actually represents (a run that has stopped, not necessarily one that finished).
Collapse the repeated staleTime literal into a shared QUERY_STALE_TIME constant, type the attention-badge Icon with SVGIconProps like the status groups do, wrap the autofix phase labels in t(), and dedupe the identical event/user count-label logic in IssueCard and IssueTableRow into one helper.
The production overview was forked from autofixIssuesDemo and still imported its RunQuestion type, duplicating the /seer/runs/ payload across three shapes (SectionRun, OverviewRunData, and the demo's SeerRun) and the issue shape (OverviewIssue vs the demo's Issue), plus the RUNS_QUERY and REQUIRED_ISSUE_FILTER constants. Make the overview the single owner: types.ts now defines the canonical RunQuestion, RunPullRequest, SeerRun, and OverviewIssue shapes and both query constants. The per-issue runs+state join is extracted into a shared useIssueAutofixEnrichment hook; sectionIssueCard consumes it, dropping its inline reimplementation, SectionRun, and RUNS_QUERY. buildOverviewRow now takes the canonical SeerRun, deleting the loosened OverviewRunData. The demo consumes the shared types and constants and extends OverviewIssue for its two extra columns. It keeps its own useQueries fan-out over a page of issues rather than the per-issue hook, which does not fit that batching shape. No autofixIssuesDemo imports remain under seerWorkflows/.
IssueCard and IssueTableRow both rendered the headline-or-title link with the "Raw issue title" tooltip verbatim. Extract a single IssueTitleLink component; the only difference between the two sites was the wrapping Text size, which is now an optional prop (the card passes size="lg", the table row uses the default).
The overview bucketed every issue server-side via issue.autofix_state, then each card independently re-derived its own state from the per-issue enrichment (deriveAutofixOutcomes -> getAttentionReason). Two classifiers over two data sources meant a card could contradict its own section header (a Merged tag inside "Awaiting your review"). Collapse the four hand-encoded stage orderings (SECTION_ORDER, OUTCOME_ORDER, the getAttentionReason ladder, the STATUS_GROUP_META fill numbers) into a single PIPELINE table in types.ts. SECTION_ORDER, the section-header checklist fill, and the focus-mode fallback precedence all derive from it. Thread the server section key through the card and let deriveCardAction map it to the primary action; delete getAttentionReason, deriveAutofixOutcomes, and OUTCOME_ORDER. OverviewRow no longer shreds one status into outcomes / prMerged / isProcessing / autofixRunStatus: it carries a single runStatus mirroring the payload, and the Running tag, Retry/Add-context prompts, pending-question line, and loading placeholder are transient overlays over the section-driven action, never a second classification. Focus mode (?id=), which has no section, reconstructs one client-side with deriveSectionKey by walking the same PIPELINE table.
index.tsx was a ~370-line god component doing feature gating, query-param decodes, two data hooks, localStorage state, the toolbar, focus mode, and the section list, with its content region a five-deep nested ternary interleaving focus and list modes. Extract three focused components alongside it: OverviewFilters (the toolbar), FocusedIssue (the ?id= pinned-issue query and its error / pending / not-found / card branches), and SectionList (useAutofixSections plus the per-section rendering). index.tsx is now a thin shell that decodes the URL, owns the shared view and collapse state, and composes the three behind the feature gate as a single focus-vs-list branch. Pure mechanical extraction: no behavior change. SortValue and OverviewView move to types.ts so the pieces can share them.
Each rendered overview card mounts two live enrichment queries, and sections fetch up to 100 issues; with LazyRender never unmounting once visible, a fully-scrolled page accumulated up to ~1000 live subscriptions all refetching on window focus. Cap each section at 25 hydrated rows via Collapsible and reveal the rest on demand. Unrevealed cards stay unmounted (Collapsible slices before rendering), so their queries never start until requested. The section header badge keeps reporting the full X-Hits total.
…vations The overview spec re-inlined near-identical run and autofix-state blocks across many tests, some carrying fields the code never reads. Introduce makeRun / makeAutofixState / makeBlock / makeOutput builders so each test states only the dimension it varies, and drop mock fields nothing reads (run type/dateCreated, output key, PR mergedAt, block id/timestamp, and the unread repo_pr_states fields). Every prior assertion is preserved. Add direct coverage for the pure derivation logic, which the component tests only exercised indirectly: - buildOverviewRows.spec: parseRootCause (pipe split, no-pipe, over-length headline, empty parts, emphasis strip), normalizeBulletList (inline bullet rewrite, no-space bullet, no-op), extractPendingQuestion (nested question, flat key fallbacks, blank/missing guards, status guard), and a table-driven deriveSectionKey precedence check down to the floor. - cardAction.spec: deriveCardAction per stage key (review_pr carries the PR), and IssuePrimaryAction's pending placeholder plus the transient runStatus overlays that never reclassify the section anchor. - triggerBadge.spec: mapRunSourceToTrigger mapping and the raw-source fallback badge. Export parseRootCause / normalizeBulletList / extractPendingQuestion so they can be unit-tested next to their module.
3b9b82f to
0a3a7da
Compare
Make the overview derivations total and the period window label a whole translatable unit: - mostRecentTimestamp now compares candidates by parsed epoch time instead of lexicographically, so a higher-precision timestamp no longer loses to an earlier one that happens to sort later. - normalizeBulletList trims and filters segments before prefixing "- ", so consecutive or trailing bullets no longer leave blank list items. - periodWindowLabel returns a complete translated phrase per period rather than interpolating a lowercased fragment into another translation, and its default lookup is total (no non-null assertion). - Correct the lastActivityAt comment: it labels the card's updated TimeSince, it does not drive sorting or the period filter.
Cover the overview derivations and fallback branches that had no tests: - buildOverviewRows: mostRecentTimestamp numeric ordering and the all-nullish case, the coding_agents-only deriveSectionKey path, buildAnalysis positional fallback and empty-answer dropping, the normalizeBulletList empty-item cases, and the extractPatchInfo file/changed-line boundaries plus multi-repo path prefixing (the boundaries are pinned to the exported constants). - index: X-Hits body-length count fallback, SECTION_ORDER header ordering, the review_pr internal-link fallback when a run has no PR url, the focused-issue empty and error branches, and a section retry-refetch after an error. - periods: the per-period window phrase, the unknown-period default, and that the default period is present in the option list. Exports mostRecentTimestamp, buildAnalysis, extractPatchInfo, and the inline-diff limit constants so the derivations can be tested directly.
Sections fetch at most 100 issues but the badge showed the exact X-Hits total, overstating what scrolling could reveal.
Move the primary-action render matrix down to cardAction.spec, where it belongs, and drop the redundant full-page mounts that only re-proved it. cardAction.spec gains an it.each covering every non-overlay section action (merged, code_changes_ready, solution_ready — previously untested —, and needs_investigation) plus the internal review-link fallback for a review_pr card with no PR url. index.spec folds the header-ordering loop, the dropped-filter negatives, and the on-card analysis assertions into the two mounts that already render them, then deletes the standalone header-order, filter, thought-order, internal-review-link, Investigate, and merged-tag tests. Section-wiring stays proven by the section-authoritative merged test. Net: fewer full-page mounts, same behavioral coverage.
Errored sections were counted as empty, so a page with some sections failing and the rest empty showed the global "no runs" box and never rendered the per-section load error. Exclude errored sections from the all-empty check so the section list and its inline error surface. Show a filter-aware empty message when a project is selected or a non-default period is active, so an empty result under filters reads as "no matches" instead of "no runs yet". Expose enrichmentPending (state or runs query pending) from the enrichment hook and drive the card's primary-action placeholder from it. In focus mode the section is derived from enrichment, and merge evidence lives only on the run, so deriving before the runs query settles could transiently misclassify a merged issue. Replace the isLast border prop with a :last-child CSS rule on the table rows' container, dropping the prop from the row and every call site.
Remove the 25-row render cap and the Collapsible show-more/less wrapper so sections render every fetched issue. The fetch limit and the 100+ count badge are unchanged. Because there is no longer a cap, height-stabilize lazy cards: while enrichment is pending the mounted card reserves at least the LazyRender placeholder height, so newly mounted cards can't collapse and cascade intersection mounts. The constraint releases once enrichment settles. Update tests: drop the show-more cap test in favor of asserting all 30 fetched issues render, cover the errored-plus-empty and filtered empty states, and select two projects so the plain empty-state test keeps an empty (no-filter) selection.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9b522b3. Configure here.
| sectionKey?: AutofixStateKey; | ||
| }) { | ||
| const {run, state, enrichmentPending} = useIssueAutofixEnrichment(issue.id); | ||
| const row = buildOverviewRow(issue, run, state, enrichmentPending, statsPeriod); |
There was a problem hiding this comment.
Action waits on runs fetch
Medium Severity
buildOverviewRow is given enrichmentPending for the row's statePending field, but that field is documented and consumed as “autofix state still in flight.” IssuePrimaryAction therefore keeps showing the … placeholder until the runs request finishes too, even though section actions and live-status overlays only need state. Cards stay actionless longer than necessary after state has already resolved.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9b522b3. Configure here.
| <Text as="p" variant="muted" align="center"> | ||
| {hasNonDefaultFilters | ||
| ? t('No autofix runs match your filters.') | ||
| : t('No completed autofix runs yet.')} |
There was a problem hiding this comment.
Wrong empty-state filter check
Low Severity
hasNonDefaultFilters treats any non-empty projects selection as an active filter. Page filters auto-select the only project in single-project orgs, so those users get “No autofix runs match your filters.” on a truly empty overview instead of “No completed autofix runs yet,” even with the default period.
Reviewed by Cursor Bugbot for commit 9b522b3. Configure here.
…20397) List cards now drop the action placeholder as soon as the autofix state request resolves, instead of also waiting on the runs request, which only feeds the analysis prose and trigger badge. Focus mode (`?id=`) still waits for both requests before classifying the card, since merge evidence comes from the run's pull requests and classifying early could briefly mislabel a merged issue as awaiting review. The empty-state copy also stops counting a forced project selection as an active filter. Page filters auto-select the org's only project in single-project orgs, so a truly empty overview showed "No autofix runs match your filters." even with no filtering possible. A selection now counts as a filter only when the org has more than one project, mirroring the page-filters force-select rule. Both address review findings surfaced on #120294 after it merged.


On top of the existing autofix overview page work, this changes how the page's five sections (Awaiting your review / Code changes ready / Ready to generate code / Needs investigation / Merged) are populated:
issue.autofix_statesearch key, with true per-section counts from theX-Hitsheader — replacing the client-side classification that fetched full run state from Seer per issue (2 Seer round trips each) and could only count the current page of 10.LazyRender, so off-screen cards cost nothing.getStatusGroup/buildOverviewRowsclient classification deleted; the card/table view toggle is unaffected.Depends on the backend search key landing first: #120293. Until it does, the flag-gated page renders its error state.