Skip to content

Commit a44f814

Browse files
committed
support file details when expand
1 parent e1a3cfe commit a44f814

File tree

3 files changed

+176
-16
lines changed

3 files changed

+176
-16
lines changed

crates/chat-cli/src/cli/chat/capture.rs

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use chrono::{
99
DateTime,
1010
Local,
1111
};
12+
use crossterm::style::Stylize;
1213
use eyre::{
1314
Result,
1415
bail,
@@ -21,7 +22,6 @@ use serde::{
2122

2223
use crate::cli::ConversationState;
2324
use crate::os::Os;
24-
2525
// The shadow repo path that MUST be appended with a session-specific directory
2626
// pub const SHADOW_REPO_DIR: &str = "/Users/aws/.amazonq/cli-captures/";
2727

@@ -43,6 +43,16 @@ pub struct CaptureManager {
4343
/// If true, delete the current session's shadow repo directory when dropped.
4444
#[serde(default)]
4545
pub clean_on_drop: bool,
46+
/// Track file changes for each capture
47+
#[serde(default)]
48+
pub file_changes: HashMap<String, FileChangeStats>,
49+
}
50+
51+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
52+
pub struct FileChangeStats {
53+
pub added: usize,
54+
pub modified: usize,
55+
pub deleted: usize,
4656
}
4757

4858
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -109,9 +119,82 @@ impl CaptureManager {
109119
last_user_message: None,
110120
user_message_lock: false,
111121
clean_on_drop: false,
122+
file_changes: HashMap::new(),
112123
})
113124
}
114125

126+
pub fn get_file_changes(&self, tag: &str) -> Result<FileChangeStats> {
127+
let git_dir_arg = format!("--git-dir={}", self.shadow_repo_path.display());
128+
129+
// Get diff stats against previous tag
130+
let prev_tag = if tag == "0" {
131+
return Ok(FileChangeStats::default());
132+
} else {
133+
self.get_previous_tag(tag)?
134+
};
135+
136+
let output = Command::new("git")
137+
.args([&git_dir_arg, "diff", "--name-status", &prev_tag, tag])
138+
.output()?;
139+
140+
if !output.status.success() {
141+
bail!("Failed to get diff stats: {}", String::from_utf8_lossy(&output.stderr));
142+
}
143+
144+
let mut stats = FileChangeStats::default();
145+
for line in String::from_utf8_lossy(&output.stdout).lines() {
146+
if let Some(first_char) = line.chars().next() {
147+
match first_char {
148+
'A' => stats.added += 1,
149+
'M' => stats.modified += 1,
150+
'D' => stats.deleted += 1,
151+
_ => {},
152+
}
153+
}
154+
}
155+
156+
Ok(stats)
157+
}
158+
159+
fn get_previous_tag(&self, tag: &str) -> Result<String> {
160+
// Parse tag format "X" or "X.Y" to get previous
161+
if let Ok(turn) = tag.parse::<usize>() {
162+
if turn > 0 {
163+
return Ok((turn - 1).to_string());
164+
}
165+
} else if tag.contains('.') {
166+
let parts: Vec<&str> = tag.split('.').collect();
167+
if parts.len() == 2 {
168+
if let Ok(tool_num) = parts[1].parse::<usize>() {
169+
if tool_num > 1 {
170+
return Ok(format!("{}.{}", parts[0], tool_num - 1));
171+
} else {
172+
return Ok(parts[0].to_string());
173+
}
174+
}
175+
}
176+
}
177+
Ok("0".to_string())
178+
}
179+
180+
pub fn create_capture_with_stats(
181+
&mut self,
182+
tag: &str,
183+
commit_message: &str,
184+
history_index: usize,
185+
is_turn: bool,
186+
tool_name: Option<String>,
187+
) -> Result<()> {
188+
self.create_capture(tag, commit_message, history_index, is_turn, tool_name)?;
189+
190+
// Store file change stats
191+
if let Ok(stats) = self.get_file_changes(tag) {
192+
self.file_changes.insert(tag.to_string(), stats);
193+
}
194+
195+
Ok(())
196+
}
197+
115198
pub fn create_capture(
116199
&mut self,
117200
tag: &str,
@@ -199,20 +282,40 @@ impl CaptureManager {
199282
Ok(())
200283
}
201284

202-
pub fn diff(&self, tag1: &str, tag2: &str) -> Result<String> {
203-
let _ = self.get_capture(tag1)?;
204-
let _ = self.get_capture(tag2)?;
285+
pub fn diff_detailed(&self, tag1: &str, tag2: &str) -> Result<String> {
205286
let git_dir_arg = format!("--git-dir={}", self.shadow_repo_path.display());
206287

288+
let output = Command::new("git")
289+
.args([&git_dir_arg, "diff", "--name-status", tag1, tag2])
290+
.output()?;
291+
292+
if !output.status.success() {
293+
bail!("Failed to get diff: {}", String::from_utf8_lossy(&output.stderr));
294+
}
295+
296+
let mut result = String::new();
297+
298+
for line in String::from_utf8_lossy(&output.stdout).lines() {
299+
if let Some((status, file)) = line.split_once('\t') {
300+
match status {
301+
"A" => result.push_str(&format!(" + {} (added)\n", file).green().to_string()),
302+
"M" => result.push_str(&format!(" ~ {} (modified)\n", file).yellow().to_string()),
303+
"D" => result.push_str(&format!(" - {} (deleted)\n", file).red().to_string()),
304+
_ => {},
305+
}
306+
}
307+
}
308+
207309
let output = Command::new("git")
208310
.args([&git_dir_arg, "diff", tag1, tag2, "--stat", "--color=always"])
209311
.output()?;
210312

211313
if output.status.success() {
212-
Ok(String::from_utf8_lossy(&output.stdout).to_string())
213-
} else {
214-
bail!("Failed to get diff: {}", String::from_utf8_lossy(&output.stderr));
314+
result.push_str("\n");
315+
result.push_str(&String::from_utf8_lossy(&output.stdout));
215316
}
317+
318+
Ok(result)
216319
}
217320

218321
fn get_capture(&self, tag: &str) -> Result<&Capture> {

crates/chat-cli/src/cli/chat/cli/capture.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use eyre::Result;
1515
use crate::cli::chat::capture::{
1616
Capture,
1717
CaptureManager,
18+
FileChangeStats,
1819
};
1920
use crate::cli::chat::{
2021
ChatError,
@@ -56,7 +57,11 @@ pub enum CaptureSubcommand {
5657
Expand { tag: String },
5758

5859
/// Display a diff between two checkpoints
59-
Diff { tag1: String, tag2: String },
60+
Diff {
61+
tag1: String,
62+
#[arg(required = false)]
63+
tag2: Option<String>,
64+
},
6065
}
6166

6267
impl CaptureSubcommand {
@@ -196,9 +201,22 @@ impl CaptureSubcommand {
196201
));
197202
},
198203
},
199-
Self::Diff { tag1, tag2 } => match manager.diff(&tag1, &tag2) {
200-
Ok(diff) => execute!(session.stderr, style::Print(diff))?,
201-
Err(e) => return Err(ChatError::Custom(format!("Could not display diff: {e}").into())),
204+
Self::Diff { tag1, tag2 } => {
205+
// if only provide tag1, compare with current status
206+
let to_tag = tag2.unwrap_or_else(|| "HEAD".to_string());
207+
208+
let comparison_text = if to_tag == "HEAD" {
209+
format!("Comparing current state with checkpoint [{}]:\n", tag1)
210+
} else {
211+
format!("Comparing checkpoint [{}] with [{}]:\n", tag1, to_tag)
212+
};
213+
214+
match manager.diff_detailed(&tag1, &to_tag) {
215+
Ok(diff) => {
216+
execute!(session.stderr, style::Print(comparison_text.blue()), style::Print(diff))?;
217+
},
218+
Err(e) => return Err(ChatError::Custom(format!("Could not display diff: {e}").into())),
219+
}
202220
},
203221
}
204222

@@ -242,6 +260,45 @@ impl TryFrom<&Capture> for CaptureDisplayEntry {
242260
}
243261
}
244262

263+
impl CaptureDisplayEntry {
264+
fn with_file_stats(capture: &Capture, manager: &CaptureManager) -> Result<Self> {
265+
let mut entry = Self::try_from(capture)?;
266+
267+
if let Some(stats) = manager.file_changes.get(&capture.tag) {
268+
let stats_str = format_file_stats(stats);
269+
if !stats_str.is_empty() {
270+
entry.display_parts.push(format!(" ({})", stats_str).dark_grey());
271+
}
272+
}
273+
274+
Ok(entry)
275+
}
276+
}
277+
278+
fn format_file_stats(stats: &FileChangeStats) -> String {
279+
let mut parts = Vec::new();
280+
281+
if stats.added > 0 {
282+
parts.push(format!(
283+
"+{} file{}",
284+
stats.added,
285+
if stats.added == 1 { "" } else { "s" }
286+
));
287+
}
288+
if stats.modified > 0 {
289+
parts.push(format!("modified {}", stats.modified));
290+
}
291+
if stats.deleted > 0 {
292+
parts.push(format!(
293+
"-{} file{}",
294+
stats.deleted,
295+
if stats.deleted == 1 { "" } else { "s" }
296+
));
297+
}
298+
299+
parts.join(", ")
300+
}
301+
245302
impl std::fmt::Display for CaptureDisplayEntry {
246303
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247304
for part in self.display_parts.iter() {
@@ -265,7 +322,7 @@ fn gather_all_turn_captures(manager: &CaptureManager) -> Result<Vec<CaptureDispl
265322
if !capture.is_turn {
266323
continue;
267324
}
268-
displays.push(CaptureDisplayEntry::try_from(capture).unwrap());
325+
displays.push(CaptureDisplayEntry::with_file_stats(capture, manager)?);
269326
}
270327
Ok(displays)
271328
}
@@ -279,7 +336,7 @@ fn expand_capture(manager: &CaptureManager, output: &mut impl Write, tag: String
279336
},
280337
};
281338
let capture = &manager.captures[*capture_index];
282-
let display_entry = CaptureDisplayEntry::try_from(capture)?;
339+
let display_entry = CaptureDisplayEntry::with_file_stats(capture, manager)?;
283340
execute!(output, style::Print(display_entry), style::Print("\n"))?;
284341

285342
// If the user tries to expand a tool-level checkpoint, return early
@@ -292,7 +349,7 @@ fn expand_capture(manager: &CaptureManager, output: &mut impl Write, tag: String
292349
if capture.is_turn {
293350
break;
294351
}
295-
display_vec.push(CaptureDisplayEntry::try_from(&manager.captures[i])?);
352+
display_vec.push(CaptureDisplayEntry::with_file_stats(&manager.captures[i], manager)?);
296353
}
297354

298355
for entry in display_vec.iter().rev() {

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2362,7 +2362,7 @@ impl ChatSession {
23622362
None => tool.tool.display_name(),
23632363
};
23642364

2365-
match manager.create_capture(
2365+
match manager.create_capture_with_stats(
23662366
&tag,
23672367
&commit_message,
23682368
self.conversation.history().len() + 1,
@@ -2853,7 +2853,7 @@ impl ChatSession {
28532853
manager.num_turns += 1;
28542854
manager.num_tools_this_turn = 0;
28552855

2856-
match manager.create_capture(
2856+
match manager.create_capture_with_stats(
28572857
&manager.num_turns.to_string(),
28582858
&truncate_message(&user_message, CAPTURE_MESSAGE_MAX_LENGTH),
28592859
self.conversation.history().len(),

0 commit comments

Comments
 (0)