diff --git a/app/src/workspace/view/vertical_tabs.rs b/app/src/workspace/view/vertical_tabs.rs index e883cce112c..6a4b2a0a8d4 100644 --- a/app/src/workspace/view/vertical_tabs.rs +++ b/app/src/workspace/view/vertical_tabs.rs @@ -1953,11 +1953,16 @@ fn render_tab_group_internal( rows.finish() }; + let show_header = should_show_tab_group_header( + has_custom_title, + is_being_renamed, + visible_pane_ids.len(), + ); let group_content = if uses_outer_group_container { let mut group = Flex::column() .with_main_axis_size(MainAxisSize::Min) .with_cross_axis_alignment(CrossAxisAlignment::Stretch); - if has_custom_title || is_being_renamed { + if show_header { group.add_child(render_group_header( GroupHeaderProps { tab_index, @@ -1970,7 +1975,6 @@ fn render_tab_group_internal( )); } - let show_header = has_custom_title || is_being_renamed; let mut body_padding = Padding::uniform(0.) .with_left(GROUP_HORIZONTAL_PADDING) .with_right(GROUP_HORIZONTAL_PADDING) @@ -2950,6 +2954,30 @@ fn uses_outer_group_container(display_granularity: VerticalTabsDisplayGranularit matches!(display_granularity, VerticalTabsDisplayGranularity::Panes) } +/// Decides whether to render the tab-group header above a multi-row group in +/// `Panes` granularity. +/// +/// The header is shown when: +/// * the tab has a user-set custom title (rename flow), or +/// * the tab is currently being renamed (inline editor), or +/// * the tab contains more than one visible pane. +/// +/// The third condition is what fixes issue #9098: previously the header was +/// only shown when a custom title existed, so multi-pane tabs with auto- +/// generated names (the AI/CLI session naming flow) rendered without any +/// tab-level identifier — only their first row's title was visible, which +/// looked identical for every tab and made the bar appear "nameless". +/// Single-pane groups in `Panes` mode still omit the header because the +/// single row already shows the pane title (avoids duplicating the same +/// text immediately above itself). +fn should_show_tab_group_header( + has_custom_title: bool, + is_being_renamed: bool, + visible_pane_count: usize, +) -> bool { + has_custom_title || is_being_renamed || visible_pane_count > 1 +} + fn search_fragments_contain_query(fragments: &[String], query_lower: &str) -> bool { fragments .iter() diff --git a/app/src/workspace/view/vertical_tabs_tests.rs b/app/src/workspace/view/vertical_tabs_tests.rs index edced61be2d..294e78c2134 100644 --- a/app/src/workspace/view/vertical_tabs_tests.rs +++ b/app/src/workspace/view/vertical_tabs_tests.rs @@ -17,7 +17,8 @@ use super::{ non_terminal_search_text_fragments, pane_ids_for_display_granularity, pane_search_text_fragments, preferred_agent_tab_titles, search_fragments_contain_query, select_summary_pane_kind_icons, should_keep_detail_sidecar_visible_for_mouse_position, - summary_overflow_count, summary_search_text_fragments, terminal_kind_badge_label, + should_show_tab_group_header, summary_overflow_count, summary_search_text_fragments, + terminal_kind_badge_label, terminal_primary_line_data, terminal_pull_request_badge_label, terminal_search_text_fragments, terminal_title_fallback_font, uses_outer_group_container, visible_pane_ids_for_detail_target, vtab_diff_stats_text, AgentTabTextPreference, SummaryPaneKind, SummaryPaneKindIcons, @@ -589,6 +590,62 @@ fn tabs_granularity_does_not_use_outer_group_container() { )); } +// Regression coverage for #9098 ("Tab names not rendered in tab bar, only +// first tab shows name"). The header gate previously read `has_custom_title +// || is_being_renamed`, which collapsed to `false` for every tab without a +// user-set rename — leaving multi-pane tabs with auto-generated names +// looking like they had no tab label at all. The new gate keeps the existing +// triggers and adds "any multi-pane tab", so every multi-pane group has a +// stable tab-level identifier in `Panes` granularity. +#[test] +fn tab_group_header_shows_for_custom_title() { + assert!(should_show_tab_group_header(true, false, 1)); + assert!(should_show_tab_group_header(true, false, 3)); +} + +#[test] +fn tab_group_header_shows_while_renaming() { + // The inline rename editor must always be reachable, even on + // single-pane tabs with no prior custom title. + assert!(should_show_tab_group_header(false, true, 1)); +} + +#[test] +fn tab_group_header_shows_for_multi_pane_tabs_without_custom_title() { + // The #9098 case: an auto-named multi-pane tab. Each row only shows the + // per-pane title (e.g. `travelplan` + `main`), so without a group header + // there is no way to tell two such tabs apart in the sidebar. + assert!(should_show_tab_group_header(false, false, 2)); + assert!(should_show_tab_group_header(false, false, 5)); +} + +#[test] +fn tab_group_header_hidden_for_single_pane_without_custom_title() { + // Single-pane groups already surface the pane title in their only row. + // Rendering the same string again as a header would duplicate it + // immediately above itself, so the gate stays closed in this shape. + assert!(!should_show_tab_group_header(false, false, 1)); + // Defensive: `0` should not crash or accidentally render a header for + // an empty group (this shape shouldn't reach the renderer in practice, + // but the helper is total and stays closed). + assert!(!should_show_tab_group_header(false, false, 0)); +} + +#[test] +fn tab_group_header_distinguishes_two_auto_named_multi_pane_tabs() { + // Models the screenshot in #9098: tab 1 has a custom title + // ("Humanfigure"), tabs 2 and 3 are auto-named multi-pane groups + // ("travelplan + main", "deponti + release/development"). Before the + // fix only tab 1 showed a header; after the fix every multi-pane tab + // gets one so the user can tell them apart at a glance. + let renders_header: Vec = vec![ + should_show_tab_group_header(true, false, 2), // tab 1 + should_show_tab_group_header(false, false, 2), // tab 2 + should_show_tab_group_header(false, false, 2), // tab 3 + ]; + assert_eq!(renders_header, vec![true, true, true]); +} + #[test] fn terminal_primary_line_prefers_cli_agent_display_title() { let line = terminal_primary_line_data(