use std::path::{Path, PathBuf};
use crate::model_identity::PromptModel;
use super::{
model_prompts::{self, ModelPrompt, ModelPromptMode},
project_instructions::ProjectInstructions,
PromptSource, PromptSourceKind, SystemPrompt, BASE_SYSTEM_PROMPT,
};
#[derive(Clone, Copy)]
pub(crate) enum PromptSession {
Current,
Different,
}
#[derive(Clone)]
enum PromptPart {
Retained {
text: String,
sources: Vec<PromptSource>,
},
ProjectInstructions(ProjectInstructions),
}
#[derive(Clone)]
pub(crate) struct ModelPromptTemplate {
home: Option<PathBuf>,
before_model: String,
parts: Vec<PromptPart>,
mcp: String,
}
#[cfg(test)]
#[path = "model_prompt_template_tests.rs"]
mod tests;
impl ModelPromptTemplate {
pub(super) fn new(
home: Option<&Path>,
before_model: String,
retained: String,
sources: Vec<PromptSource>,
) -> Self {
Self {
home: home.map(Path::to_path_buf),
before_model,
parts: vec![PromptPart::Retained {
text: retained,
sources,
}],
mcp: String::new(),
}
}
pub(super) fn with_project_instructions(mut self, cwd: &Path) -> Self {
self.parts
.push(PromptPart::ProjectInstructions(ProjectInstructions::new(
cwd,
self.home.as_deref(),
)));
self
}
pub(crate) fn set_added_dirs(
&mut self,
added_dirs: &crate::added_dirs::AddedDirs,
) -> Vec<(PathBuf, String)> {
let home = self.home.clone();
self.parts
.iter_mut()
.filter_map(|part| match part {
PromptPart::ProjectInstructions(instructions) => Some(instructions),
PromptPart::Retained { .. } => None,
})
.flat_map(|instructions| {
instructions.set_added_dirs(added_dirs.as_slice(), home.as_deref())
})
.collect()
}
pub(super) fn append_section(&mut self, text: String, sources: Vec<PromptSource>) {
self.parts.push(PromptPart::Retained { text, sources });
}
pub(crate) fn append_retained(&mut self, text: &str) {
self.append_section(
text.to_owned(),
vec![PromptSource {
kind: PromptSourceKind::Base,
path: None,
bytes: text.len(),
}],
);
}
pub(crate) fn replace_mcp(&mut self, report: &crate::tools::mcp::McpSessionReport) {
self.mcp = super::mcp_context(report);
}
pub(crate) fn build(&self, running: &PromptModel) -> anyhow::Result<SystemPrompt> {
let selected = model_prompts::load(self.home.as_deref(), running)?;
Ok(self.render(running, selected.as_ref()))
}
pub(crate) fn build_for_session(
&mut self,
running: &PromptModel,
session: PromptSession,
) -> anyhow::Result<SystemPrompt> {
let selected = model_prompts::load(self.home.as_deref(), running)?;
match session {
PromptSession::Current => {}
PromptSession::Different => {
for part in &mut self.parts {
if let PromptPart::ProjectInstructions(instructions) = part {
instructions.reload(self.home.as_deref());
}
}
}
}
Ok(self.render(running, selected.as_ref()))
}
pub(crate) fn render(
&self,
running: &PromptModel,
selected: Option<&ModelPrompt>,
) -> SystemPrompt {
let mut text = String::new();
let mut sources = vec![PromptSource {
kind: PromptSourceKind::Base,
path: None,
bytes: 0,
}];
if !matches!(
selected.map(|prompt| prompt.mode),
Some(ModelPromptMode::Replace)
) {
text.push_str(BASE_SYSTEM_PROMPT);
sources[0].bytes += BASE_SYSTEM_PROMPT.len();
}
if let Some(selected) = selected {
let start = text.len();
if !text.is_empty() {
text.push_str("\n\n");
}
text.push_str(&selected.body);
sources.push(PromptSource {
kind: match selected.mode {
ModelPromptMode::Append => PromptSourceKind::ModelAppend,
ModelPromptMode::Replace => PromptSourceKind::ModelReplace,
},
path: Some(selected.path.display().to_string()),
bytes: text.len() - start,
});
}
let start = text.len();
text.push_str(&self.before_model);
text.push_str(&format!(
"You are running on {}. Rho can switch this mid-session and tells you when it does.\n",
running.describe(),
));
text.push_str(&self.mcp);
sources[0].bytes += text.len() - start;
for part in &self.parts {
match part {
PromptPart::Retained {
text: retained,
sources: retained_sources,
} => {
text.push_str(retained);
for source in retained_sources {
if source.kind == PromptSourceKind::Base {
sources[0].bytes += source.bytes;
} else {
sources.push(source.clone());
}
}
}
PromptPart::ProjectInstructions(instructions) => {
instructions.append_to(&mut text, &mut sources);
}
}
}
SystemPrompt {
text,
sources,
model_prompt: selected.cloned(),
}
}
}