@@ -23,6 +23,8 @@ import type {
2323import { withSerializeSpan } from "../telemetry.js" ;
2424import {
2525 boldUnderline ,
26+ type FixabilityTier ,
27+ fixabilityColor ,
2628 green ,
2729 levelColor ,
2830 muted ,
@@ -55,6 +57,60 @@ function capitalize(str: string): string {
5557 return str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) ;
5658}
5759
60+ /**
61+ * Convert Seer fixability score to a tier label.
62+ *
63+ * Thresholds are simplified from Sentry core (sentry/seer/autofix/constants.py)
64+ * into 3 tiers for CLI display.
65+ *
66+ * @param score - Numeric fixability score (0-1)
67+ * @returns `"high"` | `"med"` | `"low"`
68+ */
69+ export function getSeerFixabilityLabel ( score : number ) : FixabilityTier {
70+ if ( score > 0.66 ) {
71+ return "high" ;
72+ }
73+ if ( score > 0.33 ) {
74+ return "med" ;
75+ }
76+ return "low" ;
77+ }
78+
79+ /**
80+ * Format fixability score as "label(pct%)" for compact list display.
81+ *
82+ * @param score - Numeric fixability score, or null/undefined if unavailable
83+ * @returns Formatted string like `"med(50%)"`, or `""` when score is unavailable
84+ */
85+ export function formatFixability ( score : number | null | undefined ) : string {
86+ if ( score === null || score === undefined ) {
87+ return "" ;
88+ }
89+ const label = getSeerFixabilityLabel ( score ) ;
90+ const pct = Math . round ( score * 100 ) ;
91+ return `${ label } (${ pct } %)` ;
92+ }
93+
94+ /**
95+ * Format fixability score for detail view: "Label (pct%)".
96+ *
97+ * Uses capitalized label with space before parens for readability
98+ * in the single-issue detail display.
99+ *
100+ * @param score - Numeric fixability score, or null/undefined if unavailable
101+ * @returns Formatted string like `"Med (50%)"`, or `""` when score is unavailable
102+ */
103+ export function formatFixabilityDetail (
104+ score : number | null | undefined
105+ ) : string {
106+ if ( score === null || score === undefined ) {
107+ return "" ;
108+ }
109+ const label = getSeerFixabilityLabel ( score ) ;
110+ const pct = Math . round ( score * 100 ) ;
111+ return `${ capitalize ( label ) } (${ pct } %)` ;
112+ }
113+
58114/** Map of entry type strings to their TypeScript types */
59115type EntryTypeMap = {
60116 exception : ExceptionEntry ;
@@ -254,10 +310,12 @@ const COL_ALIAS = 15;
254310const COL_SHORT_ID = 22 ;
255311const COL_COUNT = 5 ;
256312const COL_SEEN = 10 ;
313+ /** Width for the FIXABILITY column (longest value "high(100%)" = 10) */
314+ const COL_FIX = 10 ;
257315
258316/** Column where title starts in single-project mode (no ALIAS column) */
259317const TITLE_START_COL =
260- COL_LEVEL + 1 + COL_SHORT_ID + 1 + COL_COUNT + 2 + COL_SEEN + 2 ; // = 50
318+ COL_LEVEL + 1 + COL_SHORT_ID + 1 + COL_COUNT + 2 + COL_SEEN + 2 + COL_FIX + 2 ;
261319
262320/** Column where title starts in multi-project mode (with ALIAS column) */
263321const TITLE_START_COL_MULTI =
@@ -270,7 +328,9 @@ const TITLE_START_COL_MULTI =
270328 COL_COUNT +
271329 2 +
272330 COL_SEEN +
273- 2 ; // = 66
331+ 2 +
332+ COL_FIX +
333+ 2 ;
274334
275335/**
276336 * Format the header row for issue list table.
@@ -291,6 +351,8 @@ export function formatIssueListHeader(isMultiProject = false): string {
291351 " " +
292352 "SEEN" . padEnd ( COL_SEEN ) +
293353 " " +
354+ "FIXABILITY" . padEnd ( COL_FIX ) +
355+ " " +
294356 "TITLE"
295357 ) ;
296358 }
@@ -303,6 +365,8 @@ export function formatIssueListHeader(isMultiProject = false): string {
303365 " " +
304366 "SEEN" . padEnd ( COL_SEEN ) +
305367 " " +
368+ "FIXABILITY" . padEnd ( COL_FIX ) +
369+ " " +
306370 "TITLE"
307371 ) ;
308372}
@@ -521,6 +585,15 @@ export function formatIssueRow(
521585 const count = `${ issue . count } ` . padStart ( COL_COUNT ) ;
522586 const seen = formatRelativeTime ( issue . lastSeen ) ;
523587
588+ // Fixability column (color applied after padding to preserve alignment)
589+ const fixText = formatFixability ( issue . seerFixabilityScore ) ;
590+ const fixPadding = " " . repeat ( Math . max ( 0 , COL_FIX - fixText . length ) ) ;
591+ const score = issue . seerFixabilityScore ;
592+ const fix =
593+ fixText && score !== null && score !== undefined
594+ ? fixabilityColor ( fixText , getSeerFixabilityLabel ( score ) ) + fixPadding
595+ : fixPadding ;
596+
524597 // Multi-project mode: include ALIAS column
525598 if ( isMultiProject ) {
526599 const aliasShorthand = computeAliasShorthand ( issue . shortId , projectAlias ) ;
@@ -529,11 +602,11 @@ export function formatIssueRow(
529602 ) ;
530603 const alias = `${ aliasShorthand } ${ aliasPadding } ` ;
531604 const title = wrapTitle ( issue . title , TITLE_START_COL_MULTI , termWidth ) ;
532- return `${ level } ${ alias } ${ shortId } ${ count } ${ seen } ${ title } ` ;
605+ return `${ level } ${ alias } ${ shortId } ${ count } ${ seen } ${ fix } ${ title } ` ;
533606 }
534607
535608 const title = wrapTitle ( issue . title , TITLE_START_COL , termWidth ) ;
536- return `${ level } ${ shortId } ${ count } ${ seen } ${ title } ` ;
609+ return `${ level } ${ shortId } ${ count } ${ seen } ${ fix } ${ title } ` ;
537610}
538611
539612/**
@@ -564,6 +637,16 @@ export function formatIssueDetails(issue: SentryIssue): string[] {
564637 lines . push ( `Priority: ${ capitalize ( issue . priority ) } ` ) ;
565638 }
566639
640+ // Seer fixability
641+ if (
642+ issue . seerFixabilityScore !== null &&
643+ issue . seerFixabilityScore !== undefined
644+ ) {
645+ const fixDetail = formatFixabilityDetail ( issue . seerFixabilityScore ) ;
646+ const tier = getSeerFixabilityLabel ( issue . seerFixabilityScore ) ;
647+ lines . push ( `Fixability: ${ fixabilityColor ( fixDetail , tier ) } ` ) ;
648+ }
649+
567650 // Level with unhandled indicator
568651 let levelLine = issue . level ?? "unknown" ;
569652 if ( issue . isUnhandled ) {
0 commit comments