1use std::path::{Path, PathBuf};
21
22use crate::error::{CoderError, Result};
23use crate::workspace_scan::scan_workspace_for_files;
24
25pub const DEFAULT_CONTEXT_CAP_CHARS: usize = 32_000;
31
32pub const WHOLE_FILE_SYSTEM_PROMPT: &str = "\
39You are a coding assistant editing files. \
40For each file you change, emit ONLY the complete updated file contents. \
41Do not include diffs, code fences, prose, or explanations. \
42Start each file with a single line: FILE: <relative path>\n\
43Then the verbatim updated file contents, followed by a line containing only END-FILE. \
44Output the COMPLETE file body only; do NOT repeat the FILE: line inside the body, \
45and do NOT emit a unified diff. \
46If you do not change a file, do not emit it. \
47Do not invent files that don't exist.\
48";
49
50#[derive(Debug, Clone)]
54pub struct CoderPrompt {
55 pub system: String,
56 pub user: String,
57 pub included_files: Vec<PathBuf>,
58}
59
60pub fn build_prompt(workspace: &Path, task: &str) -> Result<CoderPrompt> {
65 let files = scan_workspace_for_files(workspace, task)?;
66 let (file_block, included) = render_files_block(workspace, &files, DEFAULT_CONTEXT_CAP_CHARS)?;
67
68 let user = format!(
69 "Task:\n{}\n\nFiles in the workspace (verbatim contents):\n{}",
70 task.trim(),
71 file_block,
72 );
73
74 Ok(CoderPrompt {
75 system: WHOLE_FILE_SYSTEM_PROMPT.to_string(),
76 user,
77 included_files: included,
78 })
79}
80
81pub fn build_reprompt(workspace: &Path, task: &str) -> Result<CoderPrompt> {
95 let files = scan_workspace_for_files(workspace, task)?;
96 let (file_block, included) = render_files_block(workspace, &files, DEFAULT_CONTEXT_CAP_CHARS)?;
97
98 let user = format!(
99 "Your previous reply could not be applied (it was a unified diff or a \
100 diff that did not match the file). Do NOT emit a diff this time.\n\n\
101 For EACH file you change, output the COMPLETE updated file contents as:\n\
102 FILE: <relative path>\n<the entire file body>\nEND-FILE\n\n\
103 No diffs, no code fences, no prose.\n\n\
104 Task:\n{}\n\nFiles in the workspace (verbatim contents):\n{}",
105 task.trim(),
106 file_block,
107 );
108
109 Ok(CoderPrompt {
110 system: WHOLE_FILE_SYSTEM_PROMPT.to_string(),
111 user,
112 included_files: included,
113 })
114}
115
116fn render_files_block(
121 workspace: &Path,
122 files: &[PathBuf],
123 cap_chars: usize,
124) -> Result<(String, Vec<PathBuf>)> {
125 let mut out = String::new();
126 let mut included = Vec::new();
127 for path in files {
128 let abs = workspace.join(path);
129 let content = std::fs::read_to_string(&abs)
130 .map_err(|e| CoderError::Workspace(format!("read {}: {e}", abs.display())))?;
131 let block = format!("FILE: {}\n{}\nEND-FILE\n\n", path.display(), content);
132 if out.len() + block.len() > cap_chars {
133 tracing::warn!(
134 included = included.len(),
135 total = files.len(),
136 cap = cap_chars,
137 "newt-coder context cap reached; dropping remaining files"
138 );
139 break;
140 }
141 out.push_str(&block);
142 included.push(path.clone());
143 }
144 Ok((out, included))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use std::fs;
151 use tempfile::TempDir;
152
153 fn write(dir: &Path, rel: &str, contents: &str) {
154 let abs = dir.join(rel);
155 if let Some(parent) = abs.parent() {
156 fs::create_dir_all(parent).unwrap();
157 }
158 fs::write(abs, contents).unwrap();
159 }
160
161 #[test]
162 fn build_prompt_injects_mentioned_file_contents() {
163 let tmp = TempDir::new().unwrap();
164 write(tmp.path(), "src/lib.rs", "pub fn greet() {}\n");
165 write(tmp.path(), "src/unused.rs", "pub fn other() {}\n");
166
167 let p = build_prompt(tmp.path(), "Rename greet to hello in src/lib.rs").unwrap();
168 assert_eq!(p.system, WHOLE_FILE_SYSTEM_PROMPT);
169 assert!(p.user.contains("FILE: src/lib.rs"));
170 assert!(p.user.contains("pub fn greet() {}"));
171 assert!(p.user.contains("END-FILE"));
172 assert!(!p.user.contains("src/unused.rs"));
174 assert_eq!(p.included_files.len(), 1);
175 }
176
177 #[test]
178 fn build_prompt_includes_task_text_verbatim() {
179 let tmp = TempDir::new().unwrap();
180 write(tmp.path(), "src/lib.rs", "pub fn x() {}\n");
181
182 let task = "Add a panic to src/lib.rs";
183 let p = build_prompt(tmp.path(), task).unwrap();
184 assert!(p.user.contains(task));
185 }
186
187 #[test]
188 fn render_files_block_caps_at_budget() {
189 let tmp = TempDir::new().unwrap();
190 let body = "X".repeat(50);
192 write(tmp.path(), "a.rs", &body);
193 write(tmp.path(), "b.rs", &body);
194 write(tmp.path(), "c.rs", &body);
195
196 let files = vec![
197 PathBuf::from("a.rs"),
198 PathBuf::from("b.rs"),
199 PathBuf::from("c.rs"),
200 ];
201 let (block, included) = render_files_block(tmp.path(), &files, 200).unwrap();
202 assert!(
203 included.len() < files.len(),
204 "cap should have dropped at least one file"
205 );
206 assert!(
207 block.len() <= 200,
208 "block {} bytes exceeded cap 200",
209 block.len()
210 );
211 }
212
213 #[test]
214 fn render_files_block_propagates_read_error() {
215 let tmp = TempDir::new().unwrap();
216 let files = vec![PathBuf::from("does-not-exist.rs")];
217 let err = render_files_block(tmp.path(), &files, 1000).unwrap_err();
218 assert!(matches!(err, CoderError::Workspace(_)));
219 }
220
221 #[test]
222 fn whole_file_system_prompt_pins_directive() {
223 assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("ONLY the complete updated file contents"));
226 assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("FILE: <relative path>"));
227 assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("END-FILE"));
228 assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("Do not include diffs"));
229 }
230}