use std::path::{Path, PathBuf};
use crate::error::{CoderError, Result};
use crate::workspace_scan::scan_workspace_for_files;
pub const DEFAULT_CONTEXT_CAP_CHARS: usize = 32_000;
pub const WHOLE_FILE_SYSTEM_PROMPT: &str = "\
You are a coding assistant editing files. \
For each file you change, emit ONLY the complete updated file contents. \
Do not include diffs, code fences, prose, or explanations. \
Start each file with a single line: FILE: <relative path>\n\
Then the verbatim updated file contents, followed by a line containing only END-FILE. \
Output the COMPLETE file body only; do NOT repeat the FILE: line inside the body, \
and do NOT emit a unified diff. \
If you do not change a file, do not emit it. \
Do not invent files that don't exist.\
";
#[derive(Debug, Clone)]
pub struct CoderPrompt {
pub system: String,
pub user: String,
pub included_files: Vec<PathBuf>,
}
pub fn build_prompt(workspace: &Path, task: &str) -> Result<CoderPrompt> {
let files = scan_workspace_for_files(workspace, task)?;
let (file_block, included) = render_files_block(workspace, &files, DEFAULT_CONTEXT_CAP_CHARS)?;
let user = format!(
"Task:\n{}\n\nFiles in the workspace (verbatim contents):\n{}",
task.trim(),
file_block,
);
Ok(CoderPrompt {
system: WHOLE_FILE_SYSTEM_PROMPT.to_string(),
user,
included_files: included,
})
}
pub fn build_reprompt(workspace: &Path, task: &str) -> Result<CoderPrompt> {
let files = scan_workspace_for_files(workspace, task)?;
let (file_block, included) = render_files_block(workspace, &files, DEFAULT_CONTEXT_CAP_CHARS)?;
let user = format!(
"Your previous reply could not be applied (it was a unified diff or a \
diff that did not match the file). Do NOT emit a diff this time.\n\n\
For EACH file you change, output the COMPLETE updated file contents as:\n\
FILE: <relative path>\n<the entire file body>\nEND-FILE\n\n\
No diffs, no code fences, no prose.\n\n\
Task:\n{}\n\nFiles in the workspace (verbatim contents):\n{}",
task.trim(),
file_block,
);
Ok(CoderPrompt {
system: WHOLE_FILE_SYSTEM_PROMPT.to_string(),
user,
included_files: included,
})
}
fn render_files_block(
workspace: &Path,
files: &[PathBuf],
cap_chars: usize,
) -> Result<(String, Vec<PathBuf>)> {
let mut out = String::new();
let mut included = Vec::new();
for path in files {
let abs = workspace.join(path);
let content = std::fs::read_to_string(&abs)
.map_err(|e| CoderError::Workspace(format!("read {}: {e}", abs.display())))?;
let block = format!("FILE: {}\n{}\nEND-FILE\n\n", path.display(), content);
if out.len() + block.len() > cap_chars {
tracing::warn!(
included = included.len(),
total = files.len(),
cap = cap_chars,
"newt-coder context cap reached; dropping remaining files"
);
break;
}
out.push_str(&block);
included.push(path.clone());
}
Ok((out, included))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn write(dir: &Path, rel: &str, contents: &str) {
let abs = dir.join(rel);
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(abs, contents).unwrap();
}
#[test]
fn build_prompt_injects_mentioned_file_contents() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "src/lib.rs", "pub fn greet() {}\n");
write(tmp.path(), "src/unused.rs", "pub fn other() {}\n");
let p = build_prompt(tmp.path(), "Rename greet to hello in src/lib.rs").unwrap();
assert_eq!(p.system, WHOLE_FILE_SYSTEM_PROMPT);
assert!(p.user.contains("FILE: src/lib.rs"));
assert!(p.user.contains("pub fn greet() {}"));
assert!(p.user.contains("END-FILE"));
assert!(!p.user.contains("src/unused.rs"));
assert_eq!(p.included_files.len(), 1);
}
#[test]
fn build_prompt_includes_task_text_verbatim() {
let tmp = TempDir::new().unwrap();
write(tmp.path(), "src/lib.rs", "pub fn x() {}\n");
let task = "Add a panic to src/lib.rs";
let p = build_prompt(tmp.path(), task).unwrap();
assert!(p.user.contains(task));
}
#[test]
fn render_files_block_caps_at_budget() {
let tmp = TempDir::new().unwrap();
let body = "X".repeat(50);
write(tmp.path(), "a.rs", &body);
write(tmp.path(), "b.rs", &body);
write(tmp.path(), "c.rs", &body);
let files = vec![
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
PathBuf::from("c.rs"),
];
let (block, included) = render_files_block(tmp.path(), &files, 200).unwrap();
assert!(
included.len() < files.len(),
"cap should have dropped at least one file"
);
assert!(
block.len() <= 200,
"block {} bytes exceeded cap 200",
block.len()
);
}
#[test]
fn render_files_block_propagates_read_error() {
let tmp = TempDir::new().unwrap();
let files = vec![PathBuf::from("does-not-exist.rs")];
let err = render_files_block(tmp.path(), &files, 1000).unwrap_err();
assert!(matches!(err, CoderError::Workspace(_)));
}
#[test]
fn whole_file_system_prompt_pins_directive() {
assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("ONLY the complete updated file contents"));
assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("FILE: <relative path>"));
assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("END-FILE"));
assert!(WHOLE_FILE_SYSTEM_PROMPT.contains("Do not include diffs"));
}
}