Skip to main content

aster/prompt/
attachments.rs

1//! 动态附件系统
2//!
3//! 根据上下文动态生成和注入附件
4
5use 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
14/// 附件管理器
15pub struct AttachmentManager {
16    telemetry_enabled: bool,
17}
18
19impl AttachmentManager {
20    /// 创建新的附件管理器
21    pub fn new(telemetry_enabled: bool) -> Self {
22        Self { telemetry_enabled }
23    }
24
25    /// 生成所有附件
26    pub fn generate_attachments(&self, context: &PromptContext) -> Vec<Attachment> {
27        let mut attachments = Vec::new();
28
29        // AGENTS.md
30        if let Some(att) = self.generate_agents_md_attachment(context) {
31            attachments.push(att);
32        }
33
34        // Critical System Reminder
35        if let Some(ref reminder) = context.critical_system_reminder {
36            attachments.push(self.generate_critical_reminder_attachment(reminder));
37        }
38
39        // IDE Selection
40        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        // IDE Opened Files
47        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        // Diagnostics
56        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        // Memory
65        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        // Plan Mode
74        if context.plan_mode {
75            attachments.push(self.generate_plan_mode_attachment());
76        }
77
78        // Delegate Mode
79        if context.delegate_mode {
80            attachments.push(self.generate_delegate_mode_attachment());
81        }
82
83        // Git Status
84        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        // Todo List
91        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        // Custom Attachments
100        if let Some(ref custom) = context.custom_attachments {
101            attachments.extend(custom.clone());
102        }
103
104        // 按优先级排序
105        attachments.sort_by_key(|a| a.priority.unwrap_or(0));
106
107        attachments
108    }
109
110    /// 生成 AGENTS.md 附件
111    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    /// 生成批判性提醒附件
139    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), // 最高优先级
145            compute_time_ms: Some(0),
146        }
147    }
148
149    /// 生成 IDE 选择内容附件
150    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    /// 生成 IDE 打开文件附件
166    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    /// 生成诊断信息附件
188    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    /// 生成记忆附件
204    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    /// 生成计划模式附件
220    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    /// 生成委托模式附件
241    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    /// 生成 Git 状态附件
255    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    /// 获取 Git 状态
273    fn get_git_status(&self, working_dir: &Path) -> Option<GitStatusInfo> {
274        // 获取当前分支
275        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        // 获取状态
285        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        // 获取 ahead/behind 信息
315        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    /// 生成任务列表附件
345    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}