1use crate::db::Database;
22use crate::persistence::Persistence;
23
24pub async fn track_progress(
26 db: &Database,
27 session_id: &str,
28 tool_name: &str,
29 _tool_args: &str,
30 tool_result: &str,
31) {
32 let entry = match tool_name {
33 "Write" => extract_write_progress(tool_result),
34 "Edit" => extract_edit_progress(tool_result),
35 "Delete" => extract_delete_progress(tool_result),
36 "Bash" => extract_bash_progress(tool_result),
37 _ => None,
38 };
39
40 if let Some(entry) = entry {
41 append_progress(db, session_id, &entry).await;
42 }
43}
44
45pub async fn get_progress_summary(db: &Database, session_id: &str) -> Option<String> {
47 match db.get_metadata(session_id, "progress").await {
48 Ok(Some(progress)) if !progress.is_empty() => Some(format!(
49 "\n## Session Progress\n\
50 The following actions have been completed this session:\n\
51 {progress}"
52 )),
53 _ => None,
54 }
55}
56
57async fn append_progress(db: &Database, session_id: &str, entry: &str) {
58 let existing = db
59 .get_metadata(session_id, "progress")
60 .await
61 .ok()
62 .flatten()
63 .unwrap_or_default();
64
65 let lines: Vec<&str> = existing.lines().collect();
67 let mut updated = if lines.len() >= 20 {
68 lines[lines.len() - 15..].join("\n")
70 } else {
71 existing
72 };
73
74 if !updated.is_empty() {
75 updated.push('\n');
76 }
77 updated.push_str(entry);
78
79 let _ = db.set_metadata(session_id, "progress", &updated).await;
80}
81
82fn extract_write_progress(result: &str) -> Option<String> {
83 if result.contains("Created") || result.contains("Wrote") {
85 let path = result.lines().next().unwrap_or(result).trim();
86 Some(format!("- \u{2705} {path}"))
87 } else {
88 None
89 }
90}
91
92fn extract_edit_progress(result: &str) -> Option<String> {
93 if result.contains("Applied") || result.contains("edited") || result.contains("replacement") {
94 let first_line = result.lines().next().unwrap_or(result).trim();
95 let short = if first_line.len() > 80 {
96 format!("{}...", &first_line[..80])
97 } else {
98 first_line.to_string()
99 };
100 Some(format!("- \u{270f}\u{fe0f} {short}"))
101 } else {
102 None
103 }
104}
105
106fn extract_delete_progress(result: &str) -> Option<String> {
107 if result.contains("Deleted") || result.contains("removed") {
108 let first_line = result.lines().next().unwrap_or(result).trim();
109 Some(format!("- \u{1f5d1}\u{fe0f} {first_line}"))
110 } else {
111 None
112 }
113}
114
115fn extract_bash_progress(result: &str) -> Option<String> {
116 if result.contains("Background process started") {
118 let cmd = result
120 .lines()
121 .find(|l| l.trim_start().starts_with("Command:"))
122 .and_then(|l| l.split_once(':').map(|(_, v)| v))
123 .map(|s| s.trim())
124 .unwrap_or("?");
125 let short = if cmd.len() > 60 {
126 format!("{}...", &cmd[..60])
127 } else {
128 cmd.to_string()
129 };
130 return Some(format!("- \u{1f4e1} Started background: {short}"));
131 }
132 let lower = result.to_lowercase();
134 if lower.contains("test result: ok") || lower.contains("tests passed") {
135 Some("- \u{2705} Tests passed".to_string())
136 } else if lower.contains("test result: failed") || lower.contains("tests failed") {
137 Some("- \u{274c} Tests failed".to_string())
138 } else if lower.contains("build succeeded")
139 || lower.contains("finished") && lower.contains("target")
140 {
141 Some("- \u{1f3d7}\u{fe0f} Build succeeded".to_string())
142 } else if lower.contains("error:") && lower.contains("could not compile") {
143 Some("- \u{274c} Build failed".to_string())
144 } else {
145 None
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_write_progress() {
155 assert!(extract_write_progress("Created file: src/main.rs").is_some());
156 assert!(extract_write_progress("Wrote 100 bytes to foo.rs").is_some());
157 assert!(extract_write_progress("Error: permission denied").is_none());
158 }
159
160 #[test]
161 fn test_edit_progress() {
162 assert!(extract_edit_progress("Applied 2 replacements to src/lib.rs").is_some());
163 assert!(extract_edit_progress("No changes needed").is_none());
164 }
165
166 #[test]
167 fn test_bash_progress() {
168 assert!(extract_bash_progress("test result: ok. 50 passed").is_some());
169 assert!(extract_bash_progress("test result: FAILED. 1 failed").is_some());
170 assert!(extract_bash_progress("hello world").is_none());
171 }
172
173 #[test]
174 fn test_delete_progress() {
175 assert!(extract_delete_progress("Deleted src/old.rs").is_some());
176 assert!(extract_delete_progress("File not found").is_none());
177 }
178}