From 7142e6a7817a3d88e37b41eaac156d08f6a41d4a Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Fri, 9 Jan 2026 11:45:53 -0300
Subject: [PATCH] agent_ui: Add per-file and total number of lines added and
removed (#46454)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This PR adds the total and per-file number of lines added and removed
per thread, using the same logic to compute these as in the commit view.
Here's how they show up in the UI:
Release Notes:
- Agent: Added the total and per-file number of lines added and removed
in a thread.
---
crates/agent_ui/src/acp/thread_view.rs | 74 +++++++++++++++++++++++---
crates/ui/src/components/diff_stat.rs | 11 +++-
2 files changed, 76 insertions(+), 9 deletions(-)
diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs
index 995a230e146288ba94cf991f7118f930ec9f55a1..c0206285a1ea3ef8e0ee742badbef197dee238a6 100644
--- a/crates/agent_ui/src/acp/thread_view.rs
+++ b/crates/agent_ui/src/acp/thread_view.rs
@@ -47,12 +47,12 @@ use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use terminal_view::terminal_panel::TerminalPanel;
-use text::Anchor;
+use text::{Anchor, ToPoint as _};
use theme::{AgentFontSize, ThemeSettings};
use ui::{
- Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, Disclosure, Divider,
- DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip,
- WithScrollbar, prelude::*, right_click_menu,
+ Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DiffStat, Disclosure,
+ Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor,
+ Tooltip, WithScrollbar, prelude::*, right_click_menu,
};
use util::defer;
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
@@ -268,6 +268,43 @@ impl ThreadFeedbackState {
}
}
+#[derive(Default, Clone, Copy)]
+struct DiffStats {
+ lines_added: u32,
+ lines_removed: u32,
+}
+
+impl DiffStats {
+ fn single_file(buffer: &Buffer, diff: &BufferDiff, cx: &App) -> Self {
+ let mut stats = DiffStats::default();
+ let diff_snapshot = diff.snapshot(cx);
+ let buffer_snapshot = buffer.snapshot();
+ let base_text = diff_snapshot.base_text();
+
+ for hunk in diff_snapshot.hunks(&buffer_snapshot) {
+ let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
+ stats.lines_added += added_rows;
+
+ let base_start = hunk.diff_base_byte_range.start.to_point(base_text).row;
+ let base_end = hunk.diff_base_byte_range.end.to_point(base_text).row;
+ let removed_rows = base_end.saturating_sub(base_start);
+ stats.lines_removed += removed_rows;
+ }
+
+ stats
+ }
+
+ fn all_files(changed_buffers: &BTreeMap, Entity>, cx: &App) -> Self {
+ let mut total = DiffStats::default();
+ for (buffer, diff) in changed_buffers {
+ let stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
+ total.lines_added += stats.lines_added;
+ total.lines_removed += stats.lines_removed;
+ }
+ total
+ }
+}
+
pub struct AcpThreadView {
agent: Rc,
agent_server_store: Entity,
@@ -4594,12 +4631,19 @@ impl AcpThreadView {
),
)
} else {
+ let stats = DiffStats::all_files(changed_buffers, cx);
+ let dot_divider = || {
+ Label::new("•")
+ .size(LabelSize::XSmall)
+ .color(Color::Disabled)
+ };
+
this.child(
Label::new("Edits")
.size(LabelSize::Small)
.color(Color::Muted),
)
- .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
+ .child(dot_divider())
.child(
Label::new(format!(
"{} {}",
@@ -4613,6 +4657,12 @@ impl AcpThreadView {
.size(LabelSize::Small)
.color(Color::Muted),
)
+ .child(dot_divider())
+ .child(DiffStat::new(
+ "total",
+ stats.lines_added as usize,
+ stats.lines_removed as usize,
+ ))
}
})
.on_click(cx.listener(|this, _, _, cx| {
@@ -4693,7 +4743,7 @@ impl AcpThreadView {
changed_buffers
.iter()
.enumerate()
- .flat_map(|(index, (buffer, _diff))| {
+ .flat_map(|(index, (buffer, diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
@@ -4719,7 +4769,7 @@ impl AcpThreadView {
Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
- .ml_1p5()
+ .ml_1()
});
let full_path = path.display(path_style).to_string();
@@ -4739,6 +4789,8 @@ impl AcpThreadView {
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
+ let file_stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
+
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
@@ -4768,6 +4820,14 @@ impl AcpThreadView {
.child(file_icon)
.children(file_name)
.children(file_path)
+ .child(
+ DiffStat::new(
+ "file",
+ file_stats.lines_added as usize,
+ file_stats.lines_removed as usize,
+ )
+ .label_size(LabelSize::XSmall),
+ )
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Go to File",
diff --git a/crates/ui/src/components/diff_stat.rs b/crates/ui/src/components/diff_stat.rs
index 2606963555c682d9d949d19d57471e02c53351d7..ec6d515f1b4f847631fc65fae4ed3ccd3185d271 100644
--- a/crates/ui/src/components/diff_stat.rs
+++ b/crates/ui/src/components/diff_stat.rs
@@ -5,6 +5,7 @@ pub struct DiffStat {
id: ElementId,
added: usize,
removed: usize,
+ label_size: LabelSize,
}
impl DiffStat {
@@ -13,8 +14,14 @@ impl DiffStat {
id: id.into(),
added,
removed,
+ label_size: LabelSize::Small,
}
}
+
+ pub fn label_size(mut self, label_size: LabelSize) -> Self {
+ self.label_size = label_size;
+ self
+ }
}
impl RenderOnce for DiffStat {
@@ -33,7 +40,7 @@ impl RenderOnce for DiffStat {
.child(
Label::new(self.added.to_string())
.color(Color::Success)
- .size(LabelSize::Small),
+ .size(self.label_size),
),
)
.child(
@@ -47,7 +54,7 @@ impl RenderOnce for DiffStat {
.child(
Label::new(self.removed.to_string())
.color(Color::Error)
- .size(LabelSize::Small),
+ .size(self.label_size),
),
)
}