agent_ui: Adjust edited files and plan lists to fit content height (#55189)

When a plan contains many tasks (or the edited-files list is long),
items were being visually compressed into the fixed-height container
rather than scrolling.

<img width="1730" height="622" alt="image_2026-04-28_20-17-43"
src="https://github.com/user-attachments/assets/5ee7cab3-7f14-4870-b8d3-5038139bf115"
/>

Asking an agent to "create a plan with 20 tasks" is an easy repro.

**Root cause**

Both lists used a `v_flex()` as their scroll container, and flexbox
children have `flex-shrink: 1` by default. When the container hits
`max_h`, the flex algorithm compresses all children to fit instead of
overflowing into the scroll region — resulting in 20 items crammed into
160px.

**Fix**

Separate the scroll boundary from the flex layout. A plain `div`
(non-flex) as the outer scroll container lets its inner `v_flex` size
naturally — content then overflows the bounded `div` and scrolling works
correctly.

Result:
<img width="1676" height="474" alt="image_2026-04-29_10-09-24"
src="https://github.com/user-attachments/assets/caafe06e-d0bf-456c-a53f-c215bd5582da"
/>

Closes issue #54633.

> What's missing: vertical scrollbar, as it requires large refactoring
due to double mutable borrowing of `cx` (the second introduced by scroll
handle), which will slowdown review of this pr

Release Notes:

- Fixed plan and edited-files lists in the agent panel being squashed
when they contain many items
This commit is contained in:
Oleksandr Kholiavko 2026-04-29 15:14:01 +02:00 committed by GitHub
parent 0f95845eea
commit d2bb6502bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -2295,11 +2295,9 @@ impl ThreadView {
.id("edited_files_list")
.max_h_40()
.overflow_y_scroll()
.children(
sorted_buffers
.into_iter()
.enumerate()
.flat_map(|(index, (buffer, diff))| {
.child(
v_flex().children(sorted_buffers.into_iter().enumerate().flat_map(
|(index, (buffer, diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
@ -2402,7 +2400,8 @@ impl ThreadView {
.child(buttons);
Some(element)
}),
},
)),
)
.into_any_element()
}
@ -2786,66 +2785,69 @@ impl ThreadView {
.id("plan_items_list")
.max_h_40()
.overflow_y_scroll()
.children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let entry_bg = cx.theme().colors().editor_background;
let tooltip_text: SharedString = entry.content.read(cx).source().to_string().into();
.child(
v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let entry_bg = cx.theme().colors().editor_background;
let tooltip_text: SharedString =
entry.content.read(cx).source().to_string().into();
Some(
h_flex()
.id(("plan_entry_row", index))
.py_1()
.px_2()
.gap_2()
.justify_between()
.relative()
.bg(entry_bg)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.overflow_hidden()
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.min_w_0()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::InProgress => {
Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_rotate_animation(2)
.into_any_element()
}
acp::PlanEntryStatus::Completed => {
Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.color(Color::Success)
.into_any_element()
}
acp::PlanEntryStatus::Pending | _ => {
Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element()
}
})
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
)
.child(div().absolute().top_0().right_0().h_full().w_8().bg(
linear_gradient(
90.,
linear_color_stop(entry_bg, 1.),
linear_color_stop(entry_bg.opacity(0.), 0.),
),
))
.tooltip(Tooltip::text(tooltip_text)),
)
}))
Some(
h_flex()
.id(("plan_entry_row", index))
.py_1()
.px_2()
.gap_2()
.justify_between()
.relative()
.bg(entry_bg)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.overflow_hidden()
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.min_w_0()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::InProgress => {
Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_rotate_animation(2)
.into_any_element()
}
acp::PlanEntryStatus::Completed => {
Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.color(Color::Success)
.into_any_element()
}
acp::PlanEntryStatus::Pending | _ => {
Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element()
}
})
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
)
.child(div().absolute().top_0().right_0().h_full().w_8().bg(
linear_gradient(
90.,
linear_color_stop(entry_bg, 1.),
linear_color_stop(entry_bg.opacity(0.), 0.),
),
))
.tooltip(Tooltip::text(tooltip_text)),
)
})),
)
.into_any_element()
}