use crate::skills::{render_available_skills_xml, Skill};
pub fn build_system_prompt(tool_names: &[String], skills: &[Skill]) -> String {
let tools_line = if tool_names.is_empty() {
"You have no tools available.".to_string()
} else {
format!("Available tools: {}.", tool_names.join(", "))
};
let mut prompt = format!(
"You are Capo, a Rust-native coding agent running as a terminal CLI. Keep replies concise. Prefer reading the file before suggesting changes. When you need to run a shell command, use the `bash` tool.\n\n{tools_line}"
);
if tool_names.iter().any(|t| t == "read") {
prompt.push_str(&render_available_skills_xml(skills));
}
prompt
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn includes_tool_names() {
let p = build_system_prompt(&["read".into(), "bash".into()], &[]);
assert!(p.contains("read"));
assert!(p.contains("bash"));
assert!(p.contains("Capo"));
}
#[test]
fn degrades_gracefully_with_no_tools() {
let p = build_system_prompt(&[], &[]);
assert!(p.contains("no tools"));
}
#[test]
fn omits_skills_block_when_read_tool_absent() {
use crate::skills::types::{Skill, SkillSource};
use std::path::PathBuf;
let s = Skill {
name: "x".into(),
description: "d".into(),
file_path: PathBuf::from("/x.md"),
base_dir: PathBuf::from("/"),
disable_model_invocation: false,
source: SkillSource::Global,
};
let p = build_system_prompt(&["bash".into()], &[s]);
assert!(!p.contains("<available_skills>"));
}
#[test]
fn includes_skills_block_when_read_tool_present() {
use crate::skills::types::{Skill, SkillSource};
use std::path::PathBuf;
let s = Skill {
name: "x".into(),
description: "d".into(),
file_path: PathBuf::from("/x.md"),
base_dir: PathBuf::from("/"),
disable_model_invocation: false,
source: SkillSource::Global,
};
let p = build_system_prompt(&["read".into()], &[s]);
assert!(p.contains("<available_skills>"));
assert!(p.contains("<name>x</name>"));
}
}