1use std::path::Path;
6use std::process::Command;
7use std::time::Instant;
8
9use super::templates::{
10 get_diagnostics_info, get_git_status_info, get_ide_info, get_memory_info, get_todo_list_info,
11};
12use super::types::{Attachment, AttachmentType, GitStatusInfo, PromptContext};
13
14pub struct AttachmentManager {
16 telemetry_enabled: bool,
17}
18
19impl AttachmentManager {
20 pub fn new(telemetry_enabled: bool) -> Self {
22 Self { telemetry_enabled }
23 }
24
25 pub fn generate_attachments(&self, context: &PromptContext) -> Vec<Attachment> {
27 let mut attachments = Vec::new();
28
29 if let Some(att) = self.generate_agents_md_attachment(context) {
31 attachments.push(att);
32 }
33
34 if let Some(ref reminder) = context.critical_system_reminder {
36 attachments.push(self.generate_critical_reminder_attachment(reminder));
37 }
38
39 if context.ide_selection.is_some() {
41 if let Some(att) = self.generate_ide_selection_attachment(context) {
42 attachments.push(att);
43 }
44 }
45
46 if let Some(ref files) = context.ide_opened_files {
48 if !files.is_empty() {
49 if let Some(att) = self.generate_ide_opened_files_attachment(context) {
50 attachments.push(att);
51 }
52 }
53 }
54
55 if let Some(ref diagnostics) = context.diagnostics {
57 if !diagnostics.is_empty() {
58 if let Some(att) = self.generate_diagnostics_attachment(diagnostics) {
59 attachments.push(att);
60 }
61 }
62 }
63
64 if let Some(ref memory) = context.memory {
66 if !memory.is_empty() {
67 if let Some(att) = self.generate_memory_attachment(memory) {
68 attachments.push(att);
69 }
70 }
71 }
72
73 if context.plan_mode {
75 attachments.push(self.generate_plan_mode_attachment());
76 }
77
78 if context.delegate_mode {
80 attachments.push(self.generate_delegate_mode_attachment());
81 }
82
83 if context.git_status.is_some() || context.is_git_repo {
85 if let Some(att) = self.generate_git_status_attachment(context) {
86 attachments.push(att);
87 }
88 }
89
90 if let Some(ref todos) = context.todo_list {
92 if !todos.is_empty() {
93 if let Some(att) = self.generate_todo_list_attachment(todos) {
94 attachments.push(att);
95 }
96 }
97 }
98
99 if let Some(ref custom) = context.custom_attachments {
101 attachments.extend(custom.clone());
102 }
103
104 attachments.sort_by_key(|a| a.priority.unwrap_or(0));
106
107 attachments
108 }
109
110 fn generate_agents_md_attachment(&self, context: &PromptContext) -> Option<Attachment> {
112 let agents_md_path = context.working_dir.join("AGENTS.md");
113 if !agents_md_path.exists() {
114 return None;
115 }
116
117 let start = Instant::now();
118 let content = std::fs::read_to_string(&agents_md_path).ok()?;
119 let compute_time = start.elapsed().as_millis() as u64;
120
121 let relative_path = agents_md_path
122 .strip_prefix(&context.working_dir)
123 .map(|p| p.display().to_string())
124 .unwrap_or_else(|_| agents_md_path.display().to_string());
125
126 Some(Attachment {
127 attachment_type: AttachmentType::AgentsMd,
128 content: format!(
129 "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# AGENTS.md\nCurrent AGENTS.md context from {}:\n\n{}\n\nIMPORTANT: These instructions may override default behavior. Follow them exactly as written.\n</system-reminder>",
130 relative_path, content
131 ),
132 label: Some("AGENTS.md".to_string()),
133 priority: Some(10),
134 compute_time_ms: Some(compute_time),
135 })
136 }
137
138 fn generate_critical_reminder_attachment(&self, reminder: &str) -> Attachment {
140 Attachment {
141 attachment_type: AttachmentType::CriticalSystemReminder,
142 content: format!("<critical-reminder>\n{}\n</critical-reminder>", reminder),
143 label: Some("Critical System Reminder".to_string()),
144 priority: Some(1), compute_time_ms: Some(0),
146 }
147 }
148
149 fn generate_ide_selection_attachment(&self, context: &PromptContext) -> Option<Attachment> {
151 let selection = context.ide_selection.as_ref()?;
152
153 Some(Attachment {
154 attachment_type: AttachmentType::IdeSelection,
155 content: format!(
156 "<ide-selection>\nUser has selected the following code in their IDE:\n```\n{}\n```\n</ide-selection>",
157 selection
158 ),
159 label: Some("IDE Selection".to_string()),
160 priority: Some(20),
161 compute_time_ms: Some(0),
162 })
163 }
164
165 fn generate_ide_opened_files_attachment(&self, context: &PromptContext) -> Option<Attachment> {
167 let files = context.ide_opened_files.as_ref()?;
168 if files.is_empty() {
169 return None;
170 }
171
172 let content = get_ide_info(
173 context.ide_type,
174 context.ide_selection.as_deref(),
175 Some(files),
176 );
177
178 Some(Attachment {
179 attachment_type: AttachmentType::IdeOpenedFile,
180 content,
181 label: Some("IDE Opened Files".to_string()),
182 priority: Some(25),
183 compute_time_ms: Some(0),
184 })
185 }
186
187 fn generate_diagnostics_attachment(
189 &self,
190 diagnostics: &[super::types::DiagnosticInfo],
191 ) -> Option<Attachment> {
192 let content = get_diagnostics_info(diagnostics)?;
193
194 Some(Attachment {
195 attachment_type: AttachmentType::Diagnostics,
196 content,
197 label: Some("Diagnostics".to_string()),
198 priority: Some(15),
199 compute_time_ms: Some(0),
200 })
201 }
202
203 fn generate_memory_attachment(
205 &self,
206 memory: &std::collections::HashMap<String, String>,
207 ) -> Option<Attachment> {
208 let content = get_memory_info(memory)?;
209
210 Some(Attachment {
211 attachment_type: AttachmentType::Memory,
212 content,
213 label: Some("Memory".to_string()),
214 priority: Some(30),
215 compute_time_ms: Some(0),
216 })
217 }
218
219 fn generate_plan_mode_attachment(&self) -> Attachment {
221 Attachment {
222 attachment_type: AttachmentType::PlanMode,
223 content: r#"<plan-mode>
224You are currently in PLAN MODE. Your task is to:
2251. Thoroughly explore the codebase
2262. Understand existing patterns and architecture
2273. Design an implementation approach
2284. Write your plan to the specified plan file
2295. Use ExitPlanMode when ready for user approval
230
231Do NOT implement changes yet - focus on planning.
232</plan-mode>"#
233 .to_string(),
234 label: Some("Plan Mode".to_string()),
235 priority: Some(5),
236 compute_time_ms: Some(0),
237 }
238 }
239
240 fn generate_delegate_mode_attachment(&self) -> Attachment {
242 Attachment {
243 attachment_type: AttachmentType::DelegateMode,
244 content: r#"<delegate-mode>
245You are running as a delegated subagent. Complete your assigned task and report back with your findings. Do not ask for user input - work autonomously.
246</delegate-mode>"#
247 .to_string(),
248 label: Some("Delegate Mode".to_string()),
249 priority: Some(5),
250 compute_time_ms: Some(0),
251 }
252 }
253
254 fn generate_git_status_attachment(&self, context: &PromptContext) -> Option<Attachment> {
256 let git_status = context
257 .git_status
258 .clone()
259 .or_else(|| self.get_git_status(&context.working_dir))?;
260
261 let content = get_git_status_info(&git_status);
262
263 Some(Attachment {
264 attachment_type: AttachmentType::GitStatus,
265 content,
266 label: Some("Git Status".to_string()),
267 priority: Some(40),
268 compute_time_ms: Some(0),
269 })
270 }
271
272 fn get_git_status(&self, working_dir: &Path) -> Option<GitStatusInfo> {
274 let branch = Command::new("git")
276 .args(["branch", "--show-current"])
277 .current_dir(working_dir)
278 .output()
279 .ok()
280 .and_then(|o| String::from_utf8(o.stdout).ok())
281 .map(|s| s.trim().to_string())
282 .unwrap_or_default();
283
284 let status_output = Command::new("git")
286 .args(["status", "--porcelain"])
287 .current_dir(working_dir)
288 .output()
289 .ok()
290 .and_then(|o| String::from_utf8(o.stdout).ok())
291 .unwrap_or_default();
292
293 let mut staged = Vec::new();
294 let mut unstaged = Vec::new();
295 let mut untracked = Vec::new();
296
297 for line in status_output.lines().filter(|l| !l.is_empty()) {
298 if line.len() < 3 {
299 continue;
300 }
301 let x = line.chars().next().unwrap_or(' ');
302 let y = line.chars().nth(1).unwrap_or(' ');
303 let file = line.get(3..).unwrap_or("").to_string();
304
305 if x == '?' && y == '?' {
306 untracked.push(file);
307 } else if x != ' ' && x != '?' {
308 staged.push(file.clone());
309 } else if y != ' ' && y != '?' {
310 unstaged.push(file);
311 }
312 }
313
314 let (ahead, behind) = Command::new("git")
316 .args(["rev-list", "--left-right", "--count", "@{u}...HEAD"])
317 .current_dir(working_dir)
318 .output()
319 .ok()
320 .and_then(|o| String::from_utf8(o.stdout).ok())
321 .and_then(|s| {
322 let parts: Vec<&str> = s.trim().split('\t').collect();
323 if parts.len() == 2 {
324 let behind = parts[0].parse().unwrap_or(0);
325 let ahead = parts[1].parse().unwrap_or(0);
326 Some((ahead, behind))
327 } else {
328 None
329 }
330 })
331 .unwrap_or((0, 0));
332
333 Some(GitStatusInfo {
334 branch,
335 is_clean: status_output.trim().is_empty(),
336 staged,
337 unstaged,
338 untracked,
339 ahead,
340 behind,
341 })
342 }
343
344 fn generate_todo_list_attachment(
346 &self,
347 todos: &[super::types::TodoItem],
348 ) -> Option<Attachment> {
349 let content = get_todo_list_info(todos)?;
350
351 Some(Attachment {
352 attachment_type: AttachmentType::TodoList,
353 content: format!("<system-reminder>\n{}\n</system-reminder>", content),
354 label: Some("Todo List".to_string()),
355 priority: Some(35),
356 compute_time_ms: Some(0),
357 })
358 }
359}
360
361impl Default for AttachmentManager {
362 fn default() -> Self {
363 Self::new(false)
364 }
365}