sidebar: Improve behavior of "view more" button (#51105)

This PR adjusts the "View More" button in the sidebar to expose threads
in batches of 5. Once you've expanded the whole available set, a button
to collapse the list back to the default number appears at the bottom.
Similarly, as soon as you expand the list even once, a button in the
group header shows up that does the same thing.

No release notes because this is still under feature flag.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal authored and GitHub committed 2026-03-09 10:59:32 -03:00
1 parent 6810f23634
commit 171e7cb4a7
2 files changed
+169 -30

No files matched your search

+7 -1
View File
@@ -1 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.857 6.857 4.286 5.43 2.857 4M2.857 12l1.429-1.429-1.429-1.428M6.857 4.571h6.286M6.857 8h6.286M6.857 11.428h6.286"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 8H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12H7.33333" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 4L12 6L14 4" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 12L12 10L14 12" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 289 B

After

Width:  |  Height:  |  Size: 653 B

+162 -29
View File
@@ -89,6 +89,7 @@ enum ListEntry {
ViewMore {
path_list: PathList,
remaining_count: usize,
is_fully_expanded: bool,
},
NewThread {
path_list: PathList,
@@ -174,7 +175,7 @@ pub struct Sidebar {
focused_thread: Option<acp::SessionId>,
active_entry_index: Option<usize>,
collapsed_groups: HashSet<PathList>,
expanded_groups: HashSet<PathList>,
expanded_groups: HashMap<PathList, usize>,
}
impl EventEmitter<SidebarEvent> for Sidebar {}
@@ -269,7 +270,7 @@ impl Sidebar {
focused_thread: None,
active_entry_index: None,
collapsed_groups: HashSet::new(),
expanded_groups: HashSet::new(),
expanded_groups: HashMap::new(),
}
}
@@ -579,21 +580,20 @@ impl Sidebar {
}
let total = threads.len();
let show_view_more =
total > DEFAULT_THREADS_SHOWN && !self.expanded_groups.contains(&path_list);
let count = if show_view_more {
DEFAULT_THREADS_SHOWN
} else {
total
};
let extra_batches = self.expanded_groups.get(&path_list).copied().unwrap_or(0);
let threads_to_show =
DEFAULT_THREADS_SHOWN + (extra_batches * DEFAULT_THREADS_SHOWN);
let count = threads_to_show.min(total);
let is_fully_expanded = count >= total;
entries.extend(threads.into_iter().take(count));
if show_view_more {
if total > DEFAULT_THREADS_SHOWN {
entries.push(ListEntry::ViewMore {
path_list: path_list.clone(),
remaining_count: total - DEFAULT_THREADS_SHOWN,
remaining_count: total.saturating_sub(count),
is_fully_expanded,
});
}
@@ -632,10 +632,13 @@ impl Sidebar {
let had_notifications = self.has_notifications(cx);
let scroll_position = self.list_state.logical_scroll_top();
self.rebuild_contents(cx);
self.recompute_active_entry_index(cx);
self.list_state.reset(self.contents.entries.len());
self.list_state.scroll_to(scroll_position);
if had_notifications != self.has_notifications(cx) {
multi_workspace.update(cx, |_, cx| {
@@ -720,7 +723,15 @@ impl Sidebar {
ListEntry::ViewMore {
path_list,
remaining_count,
} => self.render_view_more(ix, path_list, *remaining_count, is_selected, cx),
is_fully_expanded,
} => self.render_view_more(
ix,
path_list,
*remaining_count,
*is_fully_expanded,
is_selected,
cx,
),
ListEntry::NewThread {
path_list,
workspace,
@@ -765,7 +776,11 @@ impl Sidebar {
let workspace_for_new_thread = workspace.clone();
let workspace_for_remove = workspace.clone();
// let workspace_for_activate = workspace.clone();
let path_list_for_toggle = path_list.clone();
let path_list_for_collapse = path_list.clone();
let view_more_expanded = self.expanded_groups.contains_key(path_list);
let multi_workspace = self.multi_workspace.upgrade();
let workspace_count = multi_workspace
.as_ref()
@@ -853,6 +868,25 @@ impl Sidebar {
)),
)
})
.when(view_more_expanded && !is_collapsed, |this| {
this.child(
IconButton::new(
SharedString::from(format!("project-header-collapse-{}", ix)),
IconName::ListCollapse,
)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Collapse Displayed Threads"))
.on_click(cx.listener({
let path_list_for_collapse = path_list_for_collapse.clone();
move |this, _, _window, cx| {
this.selection = None;
this.expanded_groups.remove(&path_list_for_collapse);
this.update_entries(cx);
}
})),
)
})
.when(has_threads, |this| {
this.child(
IconButton::new(ib_id, IconName::NewThread)
@@ -1031,9 +1065,18 @@ impl Sidebar {
let workspace = workspace.clone();
self.activate_thread(session_info, &workspace, window, cx);
}
ListEntry::ViewMore { path_list, .. } => {
ListEntry::ViewMore {
path_list,
is_fully_expanded,
..
} => {
let path_list = path_list.clone();
self.expanded_groups.insert(path_list);
if *is_fully_expanded {
self.expanded_groups.remove(&path_list);
} else {
let current = self.expanded_groups.get(&path_list).copied().unwrap_or(0);
self.expanded_groups.insert(path_list, current + 1);
}
self.update_entries(cx);
}
ListEntry::NewThread { workspace, .. } => {
@@ -1202,32 +1245,42 @@ impl Sidebar {
ix: usize,
path_list: &PathList,
remaining_count: usize,
is_fully_expanded: bool,
is_selected: bool,
cx: &mut Context<Self>,
) -> AnyElement {
let path_list = path_list.clone();
let id = SharedString::from(format!("view-more-{}", ix));
let count = format!("({})", remaining_count);
let (icon, label) = if is_fully_expanded {
(IconName::ListCollapse, "Collapse List")
} else {
(IconName::Plus, "View More")
};
ListItem::new(id)
.focused(is_selected)
.child(
h_flex()
.px_1()
.py_1p5()
.p_1()
.gap_1p5()
.child(
Icon::new(IconName::Plus)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("View More").color(Color::Muted))
.child(Label::new(count).color(Color::Muted).size(LabelSize::Small)),
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(label).color(Color::Muted))
.when(!is_fully_expanded, |this| {
this.child(
Label::new(format!("({})", remaining_count))
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))),
)
}),
)
.on_click(cx.listener(move |this, _, _window, cx| {
this.selection = None;
this.expanded_groups.insert(path_list.clone());
if is_fully_expanded {
this.expanded_groups.remove(&path_list);
} else {
let current = this.expanded_groups.get(&path_list).copied().unwrap_or(0);
this.expanded_groups.insert(path_list.clone(), current + 1);
}
this.update_entries(cx);
}))
.into_any_element()
@@ -1660,9 +1713,15 @@ mod tests {
)
}
ListEntry::ViewMore {
remaining_count, ..
remaining_count,
is_fully_expanded,
..
} => {
format!(" + View More ({}){}", remaining_count, selected)
if *is_fully_expanded {
format!(" - Collapse{}", selected)
} else {
format!(" + View More ({}){}", remaining_count, selected)
}
}
ListEntry::NewThread { .. } => {
format!(" [+ New Thread]{}", selected)
@@ -1824,6 +1883,78 @@ mod tests {
);
}
#[gpui::test]
async fn test_view_more_batched_expansion(cx: &mut TestAppContext) {
let project = init_test_project("/my-project", cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
let sidebar = setup_sidebar(&multi_workspace, cx);
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
// Create 17 threads: initially shows 5, then 10, then 15, then all 17 with Collapse
save_n_test_threads(17, &path_list, cx).await;
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
cx.run_until_parked();
// Initially shows 5 threads + View More (12 remaining)
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 7); // header + 5 threads + View More
assert!(entries.iter().any(|e| e.contains("View More (12)")));
// Focus and navigate to View More, then confirm to expand by one batch
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
for _ in 0..7 {
cx.dispatch_action(SelectNext);
}
cx.dispatch_action(Confirm);
cx.run_until_parked();
// Now shows 10 threads + View More (7 remaining)
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 12); // header + 10 threads + View More
assert!(entries.iter().any(|e| e.contains("View More (7)")));
// Expand again by one batch
sidebar.update_in(cx, |s, _window, cx| {
let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
s.expanded_groups.insert(path_list.clone(), current + 1);
s.update_entries(cx);
});
cx.run_until_parked();
// Now shows 15 threads + View More (2 remaining)
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 17); // header + 15 threads + View More
assert!(entries.iter().any(|e| e.contains("View More (2)")));
// Expand one more time - should show all 17 threads with Collapse button
sidebar.update_in(cx, |s, _window, cx| {
let current = s.expanded_groups.get(&path_list).copied().unwrap_or(0);
s.expanded_groups.insert(path_list.clone(), current + 1);
s.update_entries(cx);
});
cx.run_until_parked();
// All 17 threads shown with Collapse button
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 19); // header + 17 threads + Collapse
assert!(!entries.iter().any(|e| e.contains("View More")));
assert!(entries.iter().any(|e| e.contains("Collapse")));
// Click collapse - should go back to showing 5 threads
sidebar.update_in(cx, |s, _window, cx| {
s.expanded_groups.remove(&path_list);
s.update_entries(cx);
});
cx.run_until_parked();
// Back to initial state: 5 threads + View More (12 remaining)
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 7); // header + 5 threads + View More
assert!(entries.iter().any(|e| e.contains("View More (12)")));
}
#[gpui::test]
async fn test_collapse_and_expand_group(cx: &mut TestAppContext) {
let project = init_test_project("/my-project", cx).await;
@@ -1984,6 +2115,7 @@ mod tests {
ListEntry::ViewMore {
path_list: expanded_path.clone(),
remaining_count: 42,
is_fully_expanded: false,
},
// Collapsed project header
ListEntry::ProjectHeader {
@@ -2237,10 +2369,11 @@ mod tests {
cx.dispatch_action(Confirm);
cx.run_until_parked();
// All 8 threads should now be visible, no "View More"
// All 8 threads should now be visible with a "Collapse" button
let entries = visible_entries_as_strings(&sidebar, cx);
assert_eq!(entries.len(), 9); // header + 8 threads
assert_eq!(entries.len(), 10); // header + 8 threads + Collapse button
assert!(!entries.iter().any(|e| e.contains("View More")));
assert!(entries.iter().any(|e| e.contains("Collapse")));
}
#[gpui::test]