Detailed changes
@@ -1028,6 +1028,11 @@ impl ActionLog {
.collect()
}
+ /// Returns the total number of lines added and removed across all unreviewed buffers.
+ pub fn diff_stats(&self, cx: &App) -> DiffStats {
+ DiffStats::all_files(&self.changed_buffers(cx), cx)
+ }
+
/// Iterate over buffers changed since last read or edited by the model
pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> {
self.tracked_buffers
@@ -1044,6 +1049,46 @@ impl ActionLog {
}
}
+#[derive(Default, Debug, Clone, Copy)]
+pub struct DiffStats {
+ pub lines_added: u32,
+ pub lines_removed: u32,
+}
+
+impl DiffStats {
+ pub 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
+ }
+
+ pub fn all_files(
+ changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
+ 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
+ }
+}
+
#[derive(Clone)]
pub struct ActionLogTelemetry {
pub agent_telemetry_id: SharedString,
@@ -5,7 +5,7 @@ use acp_thread::{
UserMessageId,
};
use acp_thread::{AgentConnection, Plan};
-use action_log::{ActionLog, ActionLogTelemetry};
+use action_log::{ActionLog, ActionLogTelemetry, DiffStats};
use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::AgentServer;
@@ -46,7 +46,7 @@ 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, ToPoint as _};
+use text::Anchor;
use theme::AgentFontSize;
use ui::{
Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton,
@@ -156,43 +156,6 @@ 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<Buffer>, Entity<BufferDiff>>, 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 enum AcpThreadViewEvent {
FirstSendRequested { content: Vec<acp::ContentBlock> },
}
@@ -1,5 +1,6 @@
use crate::{AgentPanel, AgentPanelEvent, NewThread};
use acp_thread::ThreadStatus;
+use action_log::DiffStats;
use agent::ThreadStore;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
@@ -73,6 +74,7 @@ struct ActiveThreadInfo {
icon: IconName,
icon_from_external_svg: Option<SharedString>,
is_background: bool,
+ diff_stats: DiffStats,
}
impl From<&ActiveThreadInfo> for acp_thread::AgentSessionInfo {
@@ -98,6 +100,7 @@ struct ThreadEntry {
is_live: bool,
is_background: bool,
highlight_positions: Vec<usize>,
+ diff_stats: DiffStats,
}
#[derive(Clone)]
@@ -402,6 +405,8 @@ impl Sidebar {
}
};
+ let diff_stats = thread.action_log().read(cx).diff_stats(cx);
+
ActiveThreadInfo {
session_id,
title,
@@ -409,6 +414,7 @@ impl Sidebar {
icon,
icon_from_external_svg,
is_background,
+ diff_stats,
}
})
.collect()
@@ -472,6 +478,7 @@ impl Sidebar {
is_live: false,
is_background: false,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
});
}
}
@@ -497,6 +504,7 @@ impl Sidebar {
thread.icon_from_external_svg = info.icon_from_external_svg.clone();
thread.is_live = true;
thread.is_background = info.is_background;
+ thread.diff_stats = info.diff_stats;
}
}
@@ -1171,6 +1179,12 @@ impl Sidebar {
.highlight_positions(thread.highlight_positions.to_vec())
.status(thread.status)
.notified(has_notification)
+ .when(thread.diff_stats.lines_added > 0, |this| {
+ this.added(thread.diff_stats.lines_added as usize)
+ })
+ .when(thread.diff_stats.lines_removed > 0, |this| {
+ this.removed(thread.diff_stats.lines_removed as usize)
+ })
.selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
.focused(is_selected)
.on_click(cx.listener(move |this, _, window, cx| {
@@ -1987,6 +2001,7 @@ mod tests {
is_live: false,
is_background: false,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
}),
// Active thread with Running status
ListEntry::Thread(ThreadEntry {
@@ -2005,6 +2020,7 @@ mod tests {
is_live: true,
is_background: false,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
}),
// Active thread with Error status
ListEntry::Thread(ThreadEntry {
@@ -2023,6 +2039,7 @@ mod tests {
is_live: true,
is_background: false,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
}),
// Thread with WaitingForConfirmation status, not active
ListEntry::Thread(ThreadEntry {
@@ -2041,6 +2058,7 @@ mod tests {
is_live: false,
is_background: false,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
}),
// Background thread that completed (should show notification)
ListEntry::Thread(ThreadEntry {
@@ -2059,6 +2077,7 @@ mod tests {
is_live: true,
is_background: true,
highlight_positions: Vec::new(),
+ diff_stats: DiffStats::default(),
}),
// View More entry
ListEntry::ViewMore {
@@ -227,6 +227,12 @@ impl RenderOnce for ThreadItem {
.gradient_stop(0.8)
.group_name("thread-item");
+ let has_diff_stats = self.added.is_some() || self.removed.is_some();
+ let added_count = self.added.unwrap_or(0);
+ let removed_count = self.removed.unwrap_or(0);
+ let diff_stat_id = self.id.clone();
+ let has_worktree = self.worktree.is_some();
+
v_flex()
.id(self.id.clone())
.group("thread-item")
@@ -235,7 +241,7 @@ impl RenderOnce for ThreadItem {
.cursor_pointer()
.w_full()
.map(|this| {
- if self.worktree.is_some() {
+ if has_worktree || has_diff_stats {
this.p_2()
} else {
this.px_2().py_1()
@@ -300,35 +306,24 @@ impl RenderOnce for ThreadItem {
.gap_1p5()
.child(icon_container()) // Icon Spacing
.child(worktree_label)
- // TODO: Uncomment the elements below when we're ready to expose this data
- // .child(dot_separator())
- // .child(
- // Label::new(self.timestamp)
- // .size(LabelSize::Small)
- // .color(Color::Muted),
- // )
- // .child(
- // Label::new("•")
- // .size(LabelSize::Small)
- // .color(Color::Muted)
- // .alpha(0.5),
- // )
- // .when(has_no_changes, |this| {
- // this.child(
- // Label::new("No Changes")
- // .size(LabelSize::Small)
- // .color(Color::Muted),
- // )
- // })
- .when(self.added.is_some() || self.removed.is_some(), |this| {
+ .when(has_diff_stats, |this| {
this.child(DiffStat::new(
- self.id,
- self.added.unwrap_or(0),
- self.removed.unwrap_or(0),
+ diff_stat_id.clone(),
+ added_count,
+ removed_count,
))
}),
)
})
+ .when(!has_worktree && has_diff_stats, |this| {
+ this.child(
+ h_flex()
+ .min_w_0()
+ .gap_1p5()
+ .child(icon_container()) // Icon Spacing
+ .child(DiffStat::new(diff_stat_id, added_count, removed_count)),
+ )
+ })
.when_some(self.on_click, |this, on_click| this.on_click(on_click))
}
}