@@ -94,7 +94,10 @@ export function formatStatusLabel(status: string | undefined): string {
9494 if ( ! status ) {
9595 return `${ statusColor ( "●" , status ) } Unknown` ;
9696 }
97- return STATUS_LABELS [ status as IssueStatus ] ?? `${ statusColor ( "●" , status ) } Unknown` ;
97+ return (
98+ STATUS_LABELS [ status as IssueStatus ] ??
99+ `${ statusColor ( "●" , status ) } Unknown`
100+ ) ;
98101}
99102
100103// ─────────────────────────────────────────────────────────────────────────────
@@ -152,21 +155,134 @@ export function divider(length = 80, char = "─"): string {
152155 return muted ( char . repeat ( length ) ) ;
153156}
154157
158+ // ─────────────────────────────────────────────────────────────────────────────
159+ // Date Formatting
160+ // ─────────────────────────────────────────────────────────────────────────────
161+
162+ /**
163+ * Format a date as relative time (e.g., "2h ago", "3d ago") or short date for older dates.
164+ *
165+ * - < 1 hour: "Xm ago"
166+ * - < 24 hours: "Xh ago"
167+ * - < 3 days: "Xd ago"
168+ * - >= 3 days: Short date (e.g., "Jan 18")
169+ */
170+ export function formatRelativeTime ( dateString : string | undefined ) : string {
171+ if ( ! dateString ) {
172+ return muted ( "—" ) . padEnd ( 10 ) ;
173+ }
174+
175+ const date = new Date ( dateString ) ;
176+ const now = Date . now ( ) ;
177+ const diffMs = now - date . getTime ( ) ;
178+ const diffMins = Math . floor ( diffMs / 60_000 ) ;
179+ const diffHours = Math . floor ( diffMs / 3_600_000 ) ;
180+ const diffDays = Math . floor ( diffMs / 86_400_000 ) ;
181+
182+ let text : string ;
183+ if ( diffMins < 60 ) {
184+ text = `${ diffMins } m ago` ;
185+ } else if ( diffHours < 24 ) {
186+ text = `${ diffHours } h ago` ;
187+ } else if ( diffDays < 3 ) {
188+ text = `${ diffDays } d ago` ;
189+ } else {
190+ // Short date: "Jan 18"
191+ text = date . toLocaleDateString ( "en-US" , { month : "short" , day : "numeric" } ) ;
192+ }
193+
194+ return text . padEnd ( 10 ) ;
195+ }
196+
155197// ─────────────────────────────────────────────────────────────────────────────
156198// Issue Formatting
157199// ─────────────────────────────────────────────────────────────────────────────
158200
201+ /** Column widths for issue list table */
202+ const COL_LEVEL = 7 ;
203+ const COL_SHORT_ID = 22 ;
204+ const COL_COUNT = 5 ;
205+ const COL_SEEN = 10 ;
206+
207+ /** Column where title starts (sum of all previous columns + separators) */
208+ const TITLE_START_COL =
209+ COL_LEVEL + 1 + COL_SHORT_ID + 1 + COL_COUNT + 2 + COL_SEEN + 2 ; // = 50
210+
159211/**
160- * Format a single issue for list display (one line)
212+ * Format the header row for issue list table.
213+ * Uses same column widths as data rows to ensure alignment.
161214 */
162- export function formatIssueRow ( issue : SentryIssue ) : string {
163- const status = formatStatusIcon ( issue . status ) ;
164- const levelText = ( issue . level ?? "unknown" ) . toUpperCase ( ) . padEnd ( 7 ) ;
215+ export function formatIssueListHeader ( ) : string {
216+ return (
217+ "LEVEL" . padEnd ( COL_LEVEL ) +
218+ " " +
219+ "SHORT ID" . padEnd ( COL_SHORT_ID ) +
220+ " " +
221+ "COUNT" . padStart ( COL_COUNT ) +
222+ " " +
223+ "SEEN" . padEnd ( COL_SEEN ) +
224+ " " +
225+ "TITLE"
226+ ) ;
227+ }
228+
229+ /**
230+ * Wrap long text with indentation for continuation lines.
231+ * Breaks at word boundaries when possible.
232+ *
233+ * @param text - Text to wrap
234+ * @param startCol - Column where text starts (for indenting continuation lines)
235+ * @param termWidth - Terminal width
236+ */
237+ function wrapTitle ( text : string , startCol : number , termWidth : number ) : string {
238+ const availableWidth = termWidth - startCol ;
239+
240+ // No wrapping needed or terminal too narrow
241+ if ( text . length <= availableWidth || availableWidth < 20 ) {
242+ return text ;
243+ }
244+
245+ const indent = " " . repeat ( startCol ) ;
246+ const lines : string [ ] = [ ] ;
247+ let remaining = text ;
248+
249+ while ( remaining . length > 0 ) {
250+ if ( remaining . length <= availableWidth ) {
251+ lines . push ( remaining ) ;
252+ break ;
253+ }
254+
255+ // Find break point (prefer word boundary)
256+ let breakAt = availableWidth ;
257+ const lastSpace = remaining . lastIndexOf ( " " , availableWidth ) ;
258+ if ( lastSpace > availableWidth * 0.5 ) {
259+ breakAt = lastSpace ;
260+ }
261+
262+ lines . push ( remaining . slice ( 0 , breakAt ) . trimEnd ( ) ) ;
263+ remaining = remaining . slice ( breakAt ) . trimStart ( ) ;
264+ }
265+
266+ // First line has no indent, continuation lines do
267+ return lines . join ( `\n${ indent } ` ) ;
268+ }
269+
270+ /**
271+ * Format a single issue for list display.
272+ * Wraps long titles with proper indentation.
273+ *
274+ * @param issue - Issue to format
275+ * @param termWidth - Terminal width for wrapping (default 80)
276+ */
277+ export function formatIssueRow ( issue : SentryIssue , termWidth = 80 ) : string {
278+ const levelText = ( issue . level ?? "unknown" ) . toUpperCase ( ) . padEnd ( COL_LEVEL ) ;
165279 const level = levelColor ( levelText , issue . level ) ;
166- const count = `${ issue . count } ` . padStart ( 5 ) ;
167- const shortId = issue . shortId . padEnd ( 15 ) ;
280+ const shortId = issue . shortId . padEnd ( COL_SHORT_ID ) ;
281+ const count = `${ issue . count } ` . padStart ( COL_COUNT ) ;
282+ const seen = formatRelativeTime ( issue . lastSeen ) ;
283+ const title = wrapTitle ( issue . title , TITLE_START_COL , termWidth ) ;
168284
169- return `${ status } ${ level } ${ shortId } ${ count } ${ issue . title } ` ;
285+ return `${ level } ${ shortId } ${ count } ${ seen } ${ title } ` ;
170286}
171287
172288/**
@@ -178,7 +294,9 @@ export function formatIssueDetails(issue: SentryIssue): string[] {
178294 // Header
179295 lines . push ( `${ issue . shortId } : ${ issue . title } ` ) ;
180296 lines . push (
181- muted ( "═" . repeat ( Math . min ( 80 , issue . title . length + issue . shortId . length + 2 ) ) )
297+ muted (
298+ "═" . repeat ( Math . min ( 80 , issue . title . length + issue . shortId . length + 2 ) )
299+ )
182300 ) ;
183301 lines . push ( "" ) ;
184302
0 commit comments