@@ -239,8 +239,8 @@ async fn run_session_picker(
239239/// Returns the human-readable column header for the given sort key.
240240fn sort_key_label ( sort_key : ThreadSortKey ) -> & ' static str {
241241 match sort_key {
242- ThreadSortKey :: CreatedAt => "Creation " ,
243- ThreadSortKey :: UpdatedAt => "Last updated " ,
242+ ThreadSortKey :: CreatedAt => "Created at " ,
243+ ThreadSortKey :: UpdatedAt => "Updated at " ,
244244 }
245245}
246246
@@ -928,7 +928,7 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
928928 let metrics = calculate_column_metrics ( & state. filtered_rows , state. show_all ) ;
929929
930930 // Column headers and list
931- render_column_headers ( frame, columns, & metrics) ;
931+ render_column_headers ( frame, columns, & metrics, state . sort_key ) ;
932932 render_list ( frame, list, state, & metrics) ;
933933
934934 // Hint line
@@ -979,24 +979,31 @@ fn render_list(
979979 let labels = & metrics. labels ;
980980 let mut y = area. y ;
981981
982+ let visibility = column_visibility ( area. width , metrics, state. sort_key ) ;
983+ let max_created_width = metrics. max_created_width ;
982984 let max_updated_width = metrics. max_updated_width ;
983985 let max_branch_width = metrics. max_branch_width ;
984986 let max_cwd_width = metrics. max_cwd_width ;
985987
986- for ( idx, ( row, ( updated_label, branch_label, cwd_label) ) ) in rows[ start..end]
988+ for ( idx, ( row, ( created_label , updated_label, branch_label, cwd_label) ) ) in rows[ start..end]
987989 . iter ( )
988990 . zip ( labels[ start..end] . iter ( ) )
989991 . enumerate ( )
990992 {
991993 let is_sel = start + idx == state. selected ;
992994 let marker = if is_sel { "> " . bold ( ) } else { " " . into ( ) } ;
993995 let marker_width = 2usize ;
994- let updated_span = if max_updated_width == 0 {
995- None
996+ let created_span = if visibility . show_created {
997+ Some ( Span :: from ( format ! ( "{created_label:<max_created_width$}" ) ) . dim ( ) )
996998 } else {
999+ None
1000+ } ;
1001+ let updated_span = if visibility. show_updated {
9971002 Some ( Span :: from ( format ! ( "{updated_label:<max_updated_width$}" ) ) . dim ( ) )
1003+ } else {
1004+ None
9981005 } ;
999- let branch_span = if max_branch_width == 0 {
1006+ let branch_span = if !visibility . show_branch {
10001007 None
10011008 } else if branch_label. is_empty ( ) {
10021009 Some (
@@ -1010,7 +1017,7 @@ fn render_list(
10101017 } else {
10111018 Some ( Span :: from ( format ! ( "{branch_label:<max_branch_width$}" ) ) . cyan ( ) )
10121019 } ;
1013- let cwd_span = if max_cwd_width == 0 {
1020+ let cwd_span = if !visibility . show_cwd {
10141021 None
10151022 } else if cwd_label. is_empty ( ) {
10161023 Some (
@@ -1027,21 +1034,31 @@ fn render_list(
10271034
10281035 let mut preview_width = area. width as usize ;
10291036 preview_width = preview_width. saturating_sub ( marker_width) ;
1030- if max_updated_width > 0 {
1037+ if visibility. show_created {
1038+ preview_width = preview_width. saturating_sub ( max_created_width + 2 ) ;
1039+ }
1040+ if visibility. show_updated {
10311041 preview_width = preview_width. saturating_sub ( max_updated_width + 2 ) ;
10321042 }
1033- if max_branch_width > 0 {
1043+ if visibility . show_branch {
10341044 preview_width = preview_width. saturating_sub ( max_branch_width + 2 ) ;
10351045 }
1036- if max_cwd_width > 0 {
1046+ if visibility . show_cwd {
10371047 preview_width = preview_width. saturating_sub ( max_cwd_width + 2 ) ;
10381048 }
1039- let add_leading_gap = max_updated_width == 0 && max_branch_width == 0 && max_cwd_width == 0 ;
1049+ let add_leading_gap = !visibility. show_created
1050+ && !visibility. show_updated
1051+ && !visibility. show_branch
1052+ && !visibility. show_cwd ;
10401053 if add_leading_gap {
10411054 preview_width = preview_width. saturating_sub ( 2 ) ;
10421055 }
10431056 let preview = truncate_text ( row. display_preview ( ) , preview_width) ;
10441057 let mut spans: Vec < Span > = vec ! [ marker] ;
1058+ if let Some ( created) = created_span {
1059+ spans. push ( created) ;
1060+ spans. push ( " " . into ( ) ) ;
1061+ }
10451062 if let Some ( updated) = updated_span {
10461063 spans. push ( updated) ;
10471064 spans. push ( " " . into ( ) ) ;
@@ -1143,26 +1160,45 @@ fn format_updated_label(row: &Row) -> String {
11431160 }
11441161}
11451162
1163+ fn format_created_label ( row : & Row ) -> String {
1164+ match ( row. created_at , row. updated_at ) {
1165+ ( Some ( created) , _) => human_time_ago ( created) ,
1166+ ( None , Some ( updated) ) => human_time_ago ( updated) ,
1167+ ( None , None ) => "-" . to_string ( ) ,
1168+ }
1169+ }
1170+
11461171fn render_column_headers (
11471172 frame : & mut crate :: custom_terminal:: Frame ,
11481173 area : Rect ,
11491174 metrics : & ColumnMetrics ,
1175+ sort_key : ThreadSortKey ,
11501176) {
11511177 if area. height == 0 {
11521178 return ;
11531179 }
11541180
11551181 let mut spans: Vec < Span > = vec ! [ " " . into( ) ] ;
1156- if metrics. max_updated_width > 0 {
1182+ let visibility = column_visibility ( area. width , metrics, sort_key) ;
1183+ if visibility. show_created {
11571184 let label = format ! (
11581185 "{text:<width$}" ,
1159- text = "Updated" ,
1186+ text = "Created at" ,
1187+ width = metrics. max_created_width
1188+ ) ;
1189+ spans. push ( Span :: from ( label) . bold ( ) ) ;
1190+ spans. push ( " " . into ( ) ) ;
1191+ }
1192+ if visibility. show_updated {
1193+ let label = format ! (
1194+ "{text:<width$}" ,
1195+ text = "Updated at" ,
11601196 width = metrics. max_updated_width
11611197 ) ;
11621198 spans. push ( Span :: from ( label) . bold ( ) ) ;
11631199 spans. push ( " " . into ( ) ) ;
11641200 }
1165- if metrics . max_branch_width > 0 {
1201+ if visibility . show_branch {
11661202 let label = format ! (
11671203 "{text:<width$}" ,
11681204 text = "Branch" ,
@@ -1171,7 +1207,7 @@ fn render_column_headers(
11711207 spans. push ( Span :: from ( label) . bold ( ) ) ;
11721208 spans. push ( " " . into ( ) ) ;
11731209 }
1174- if metrics . max_cwd_width > 0 {
1210+ if visibility . show_cwd {
11751211 let label = format ! (
11761212 "{text:<width$}" ,
11771213 text = "CWD" ,
@@ -1189,11 +1225,25 @@ fn render_column_headers(
11891225/// Widths are measured in Unicode display width (not byte length) so columns
11901226/// align correctly when labels contain non-ASCII characters.
11911227struct ColumnMetrics {
1228+ max_created_width : usize ,
11921229 max_updated_width : usize ,
11931230 max_branch_width : usize ,
11941231 max_cwd_width : usize ,
1195- /// (updated_label, branch_label, cwd_label) per row.
1196- labels : Vec < ( String , String , String ) > ,
1232+ /// (created_label, updated_label, branch_label, cwd_label) per row.
1233+ labels : Vec < ( String , String , String , String ) > ,
1234+ }
1235+
1236+ /// Determines which columns to render given available terminal width.
1237+ ///
1238+ /// When the terminal is narrow, only one timestamp column is shown (whichever
1239+ /// matches the current sort key). Branch and CWD are hidden if their max
1240+ /// widths are zero (no data to show).
1241+ #[ derive( Debug , PartialEq , Eq ) ]
1242+ struct ColumnVisibility {
1243+ show_created : bool ,
1244+ show_updated : bool ,
1245+ show_branch : bool ,
1246+ show_cwd : bool ,
11971247}
11981248
11991249fn calculate_column_metrics ( rows : & [ Row ] , include_cwd : bool ) -> ColumnMetrics {
@@ -1216,8 +1266,9 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
12161266 format ! ( "…{tail}" )
12171267 }
12181268
1219- let mut labels: Vec < ( String , String , String ) > = Vec :: with_capacity ( rows. len ( ) ) ;
1220- let mut max_updated_width = UnicodeWidthStr :: width ( "Updated" ) ;
1269+ let mut labels: Vec < ( String , String , String , String ) > = Vec :: with_capacity ( rows. len ( ) ) ;
1270+ let mut max_created_width = UnicodeWidthStr :: width ( "Created at" ) ;
1271+ let mut max_updated_width = UnicodeWidthStr :: width ( "Updated at" ) ;
12211272 let mut max_branch_width = UnicodeWidthStr :: width ( "Branch" ) ;
12221273 let mut max_cwd_width = if include_cwd {
12231274 UnicodeWidthStr :: width ( "CWD" )
@@ -1226,6 +1277,7 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
12261277 } ;
12271278
12281279 for row in rows {
1280+ let created = format_created_label ( row) ;
12291281 let updated = format_updated_label ( row) ;
12301282 let branch_raw = row. git_branch . clone ( ) . unwrap_or_default ( ) ;
12311283 let branch = right_elide ( & branch_raw, 24 ) ;
@@ -1239,20 +1291,74 @@ fn calculate_column_metrics(rows: &[Row], include_cwd: bool) -> ColumnMetrics {
12391291 } else {
12401292 String :: new ( )
12411293 } ;
1294+ max_created_width = max_created_width. max ( UnicodeWidthStr :: width ( created. as_str ( ) ) ) ;
12421295 max_updated_width = max_updated_width. max ( UnicodeWidthStr :: width ( updated. as_str ( ) ) ) ;
12431296 max_branch_width = max_branch_width. max ( UnicodeWidthStr :: width ( branch. as_str ( ) ) ) ;
12441297 max_cwd_width = max_cwd_width. max ( UnicodeWidthStr :: width ( cwd. as_str ( ) ) ) ;
1245- labels. push ( ( updated, branch, cwd) ) ;
1298+ labels. push ( ( created , updated, branch, cwd) ) ;
12461299 }
12471300
12481301 ColumnMetrics {
1302+ max_created_width,
12491303 max_updated_width,
12501304 max_branch_width,
12511305 max_cwd_width,
12521306 labels,
12531307 }
12541308}
12551309
1310+ /// Computes which columns fit in the available width.
1311+ ///
1312+ /// The algorithm reserves at least `MIN_PREVIEW_WIDTH` characters for the
1313+ /// conversation preview. If both timestamp columns don't fit, only the one
1314+ /// matching the current sort key is shown.
1315+ fn column_visibility (
1316+ area_width : u16 ,
1317+ metrics : & ColumnMetrics ,
1318+ sort_key : ThreadSortKey ,
1319+ ) -> ColumnVisibility {
1320+ const MIN_PREVIEW_WIDTH : usize = 10 ;
1321+
1322+ let show_branch = metrics. max_branch_width > 0 ;
1323+ let show_cwd = metrics. max_cwd_width > 0 ;
1324+
1325+ // Calculate remaining width after all optional columns.
1326+ let mut preview_width = area_width as usize ;
1327+ preview_width = preview_width. saturating_sub ( 2 ) ; // marker
1328+ if metrics. max_created_width > 0 {
1329+ preview_width = preview_width. saturating_sub ( metrics. max_created_width + 2 ) ;
1330+ }
1331+ if metrics. max_updated_width > 0 {
1332+ preview_width = preview_width. saturating_sub ( metrics. max_updated_width + 2 ) ;
1333+ }
1334+ if show_branch {
1335+ preview_width = preview_width. saturating_sub ( metrics. max_branch_width + 2 ) ;
1336+ }
1337+ if show_cwd {
1338+ preview_width = preview_width. saturating_sub ( metrics. max_cwd_width + 2 ) ;
1339+ }
1340+
1341+ // If preview would be too narrow, hide the non-active timestamp column.
1342+ let show_both = preview_width >= MIN_PREVIEW_WIDTH ;
1343+ let show_created = if show_both {
1344+ metrics. max_created_width > 0
1345+ } else {
1346+ sort_key == ThreadSortKey :: CreatedAt
1347+ } ;
1348+ let show_updated = if show_both {
1349+ metrics. max_updated_width > 0
1350+ } else {
1351+ sort_key == ThreadSortKey :: UpdatedAt
1352+ } ;
1353+
1354+ ColumnVisibility {
1355+ show_created,
1356+ show_updated,
1357+ show_branch,
1358+ show_cwd,
1359+ }
1360+ }
1361+
12561362#[ cfg( test) ]
12571363mod tests {
12581364 use super :: * ;
@@ -1586,7 +1692,7 @@ mod tests {
15861692 let area = frame. area ( ) ;
15871693 let segments =
15881694 Layout :: vertical ( [ Constraint :: Length ( 1 ) , Constraint :: Min ( 1 ) ] ) . split ( area) ;
1589- render_column_headers ( & mut frame, segments[ 0 ] , & metrics) ;
1695+ render_column_headers ( & mut frame, segments[ 0 ] , & metrics, state . sort_key ) ;
15901696 render_list ( & mut frame, segments[ 1 ] , & state, & metrics) ;
15911697 }
15921698 terminal. flush ( ) . expect ( "flush" ) ;
@@ -1734,14 +1840,14 @@ mod tests {
17341840 " " . into( ) ,
17351841 "Sort:" . dim( ) ,
17361842 " " . into( ) ,
1737- "Creation " . magenta( ) ,
1843+ "Created at " . magenta( ) ,
17381844 ] ) ,
17391845 header,
17401846 ) ;
17411847
17421848 frame. render_widget_ref ( Line :: from ( "Type to search" . dim ( ) ) , search) ;
17431849
1744- render_column_headers ( & mut frame, columns, & metrics) ;
1850+ render_column_headers ( & mut frame, columns, & metrics, state . sort_key ) ;
17451851 render_list ( & mut frame, list, & state, & metrics) ;
17461852
17471853 let hint_line: Line = vec ! [
@@ -1855,7 +1961,7 @@ mod tests {
18551961 let area = frame. area ( ) ;
18561962 let segments =
18571963 Layout :: vertical ( [ Constraint :: Length ( 1 ) , Constraint :: Min ( 1 ) ] ) . split ( area) ;
1858- render_column_headers ( & mut frame, segments[ 0 ] , & metrics) ;
1964+ render_column_headers ( & mut frame, segments[ 0 ] , & metrics, state . sort_key ) ;
18591965 render_list ( & mut frame, segments[ 1 ] , & state, & metrics) ;
18601966 }
18611967 terminal. flush ( ) . expect ( "flush" ) ;
@@ -1965,6 +2071,50 @@ mod tests {
19652071 assert ! ( guard[ 0 ] . search_token. is_none( ) ) ;
19662072 }
19672073
2074+ #[ test]
2075+ fn column_visibility_hides_extra_date_column_when_narrow ( ) {
2076+ let metrics = ColumnMetrics {
2077+ max_created_width : 8 ,
2078+ max_updated_width : 12 ,
2079+ max_branch_width : 0 ,
2080+ max_cwd_width : 0 ,
2081+ labels : Vec :: new ( ) ,
2082+ } ;
2083+
2084+ let created = column_visibility ( 30 , & metrics, ThreadSortKey :: CreatedAt ) ;
2085+ assert_eq ! (
2086+ created,
2087+ ColumnVisibility {
2088+ show_created: true ,
2089+ show_updated: false ,
2090+ show_branch: false ,
2091+ show_cwd: false ,
2092+ }
2093+ ) ;
2094+
2095+ let updated = column_visibility ( 30 , & metrics, ThreadSortKey :: UpdatedAt ) ;
2096+ assert_eq ! (
2097+ updated,
2098+ ColumnVisibility {
2099+ show_created: false ,
2100+ show_updated: true ,
2101+ show_branch: false ,
2102+ show_cwd: false ,
2103+ }
2104+ ) ;
2105+
2106+ let wide = column_visibility ( 40 , & metrics, ThreadSortKey :: CreatedAt ) ;
2107+ assert_eq ! (
2108+ wide,
2109+ ColumnVisibility {
2110+ show_created: true ,
2111+ show_updated: true ,
2112+ show_branch: false ,
2113+ show_cwd: false ,
2114+ }
2115+ ) ;
2116+ }
2117+
19682118 #[ tokio:: test]
19692119 async fn toggle_sort_key_reloads_with_new_sort ( ) {
19702120 let recorded_requests: Arc < Mutex < Vec < PageLoadRequest > > > = Arc :: new ( Mutex :: new ( Vec :: new ( ) ) ) ;
0 commit comments