1use ignore::WalkBuilder;
2use std::path::Path;
3
4const MAX_FILE_BYTES: usize = 32_000;
5const MAX_TREE_ENTRIES: usize = 200;
6
7pub struct WorkspaceContext {
9 pub file_tree: String,
10 pub git_status: Option<String>,
11 pub file_contents: Vec<(String, String)>, }
13
14impl WorkspaceContext {
15 pub fn build(workspace: &Path) -> Self {
16 let file_tree = build_tree(workspace);
17 let git_status = get_git_status(workspace);
18 Self {
19 file_tree,
20 git_status,
21 file_contents: Vec::new(),
22 }
23 }
24
25 pub fn add_file(&mut self, path: &str, content: &str) {
26 let truncated = if content.len() > MAX_FILE_BYTES {
27 format!(
28 "{}…[truncated at {}KB]",
29 &content[..MAX_FILE_BYTES],
30 MAX_FILE_BYTES / 1024
31 )
32 } else {
33 content.to_string()
34 };
35 self.file_contents.push((path.to_string(), truncated));
36 }
37
38 pub fn to_context_string(&self) -> String {
39 let mut out = String::new();
40
41 out.push_str("## Workspace File Tree\n```\n");
42 out.push_str(&self.file_tree);
43 out.push_str("\n```\n\n");
44
45 if let Some(ref status) = self.git_status {
46 out.push_str("## Git Status\n```\n");
47 out.push_str(status);
48 out.push_str("\n```\n\n");
49 }
50
51 if !self.file_contents.is_empty() {
52 out.push_str("## Referenced Files\n\n");
53 for (path, content) in &self.file_contents {
54 out.push_str(&format!("### {}\n```\n{}\n```\n\n", path, content));
55 }
56 }
57
58 out
59 }
60}
61
62fn build_tree(workspace: &Path) -> String {
63 let mut entries = Vec::new();
64 let walker = WalkBuilder::new(workspace)
65 .hidden(false)
66 .ignore(true)
67 .git_ignore(true)
68 .max_depth(Some(4))
69 .build();
70
71 for entry in walker.flatten() {
72 let rel = entry
73 .path()
74 .strip_prefix(workspace)
75 .unwrap_or(entry.path())
76 .display()
77 .to_string();
78 if rel.is_empty() {
79 continue;
80 }
81 let suffix = if entry.path().is_dir() { "/" } else { "" };
82 entries.push(format!("{}{}", rel, suffix));
83 if entries.len() >= MAX_TREE_ENTRIES {
84 entries.push(format!("[...and more]"));
85 break;
86 }
87 }
88 entries.sort();
89 entries.join("\n")
90}
91
92fn get_git_status(workspace: &Path) -> Option<String> {
93 let output = std::process::Command::new("git")
94 .args(["status", "--short"])
95 .current_dir(workspace)
96 .output()
97 .ok()?;
98
99 if !output.status.success() {
100 return None;
101 }
102
103 let s = String::from_utf8_lossy(&output.stdout).to_string();
104 if s.trim().is_empty() {
105 None
106 } else {
107 Some(s)
108 }
109}