use crate::config::workflows::WorkflowsConfig;
use crate::config::{PhasesConfig, StatesConfig};
#[derive(Debug, Clone)]
pub struct PromptContext<'a> {
pub status: &'a str,
pub phase: Option<&'a str>,
pub states_config: &'a StatesConfig,
pub phases_config: &'a PhasesConfig,
}
impl<'a> PromptContext<'a> {
pub fn new(
status: &'a str,
phase: Option<&'a str>,
states_config: &'a StatesConfig,
phases_config: &'a PhasesConfig,
) -> Self {
Self {
status,
phase,
states_config,
phases_config,
}
}
}
pub fn load_prompt(trigger: &str, workflows: &WorkflowsConfig) -> Option<String> {
workflows.get_prompt(trigger).map(|s| s.to_string())
}
pub fn expand_prompt(content: &str, ctx: &PromptContext) -> String {
let mut result = content.to_string();
result = result.replace("{{current_status}}", ctx.status);
if result.contains("{{valid_exits}}") {
let exits = ctx.states_config.get_exits(ctx.status);
let exits_md = if exits.is_empty() {
"- _(no transitions available - terminal state)_".to_string()
} else {
exits
.iter()
.map(|s| format!("- `{}`", s))
.collect::<Vec<_>>()
.join("\n")
};
result = result.replace("{{valid_exits}}", &exits_md);
}
if result.contains("{{current_phase}}") {
let phase_str = ctx
.phase
.map(|p| format!("`{}`", p))
.unwrap_or_else(|| "_(none)_".to_string());
result = result.replace("{{current_phase}}", &phase_str);
}
if result.contains("{{valid_phases}}") {
let mut phases: Vec<&str> = ctx.phases_config.phase_names();
phases.sort();
let phases_str = phases.join(", ");
result = result.replace("{{valid_phases}}", &phases_str);
}
result
}
pub fn get_transition_triggers(
old_status: &str,
old_phase: Option<&str>,
new_status: &str,
new_phase: Option<&str>,
) -> Vec<String> {
let mut triggers = Vec::new();
let status_changed = old_status != new_status;
let phase_changed = old_phase != new_phase;
if (status_changed || phase_changed) && old_phase.is_some() {
if let Some(op) = old_phase {
triggers.push(format!("exit~{}%{}", old_status, op));
}
}
if phase_changed {
if let Some(op) = old_phase {
triggers.push(format!("exit%{}", op));
}
}
if status_changed {
triggers.push(format!("exit~{}", old_status));
}
if status_changed {
triggers.push(format!("enter~{}", new_status));
}
if phase_changed {
if let Some(np) = new_phase {
triggers.push(format!("enter%{}", np));
}
}
if (status_changed || phase_changed) && new_phase.is_some() {
if let Some(np) = new_phase {
triggers.push(format!("enter~{}%{}", new_status, np));
}
}
triggers
}
pub fn get_transition_prompts(
old_status: &str,
old_phase: Option<&str>,
new_status: &str,
new_phase: Option<&str>,
workflows: &WorkflowsConfig,
) -> Vec<String> {
get_transition_triggers(old_status, old_phase, new_status, new_phase)
.iter()
.filter_map(|trigger| load_prompt(trigger, workflows))
.collect()
}
pub fn get_transition_prompts_with_context(
old_status: &str,
old_phase: Option<&str>,
new_status: &str,
new_phase: Option<&str>,
workflows: &WorkflowsConfig,
ctx: &PromptContext,
) -> Vec<String> {
get_transition_triggers(old_status, old_phase, new_status, new_phase)
.iter()
.filter_map(|trigger| load_prompt(trigger, workflows))
.map(|content| expand_prompt(&content, ctx))
.collect()
}
pub fn list_available_prompts(workflows: &WorkflowsConfig) -> Vec<String> {
workflows.list_prompt_triggers()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_triggers_status_change_only() {
let triggers = get_transition_triggers("pending", None, "working", None);
assert_eq!(triggers, vec!["exit~pending", "enter~working"]);
}
#[test]
fn test_triggers_phase_change_only() {
let triggers =
get_transition_triggers("working", Some("diagnose"), "working", Some("review"));
assert_eq!(
triggers,
vec![
"exit~working%diagnose",
"exit%diagnose",
"enter%review",
"enter~working%review"
]
);
}
#[test]
fn test_triggers_both_change() {
let triggers =
get_transition_triggers("working", Some("diagnose"), "finished", Some("review"));
assert_eq!(
triggers,
vec![
"exit~working%diagnose",
"exit%diagnose",
"exit~working",
"enter~finished",
"enter%review",
"enter~finished%review"
]
);
}
#[test]
fn test_triggers_enter_phase_from_none() {
let triggers = get_transition_triggers("working", None, "working", Some("diagnose"));
assert_eq!(triggers, vec!["enter%diagnose", "enter~working%diagnose"]);
}
#[test]
fn test_triggers_exit_phase_to_none() {
let triggers = get_transition_triggers("working", Some("diagnose"), "working", None);
assert_eq!(triggers, vec!["exit~working%diagnose", "exit%diagnose"]);
}
#[test]
fn test_no_triggers_when_unchanged() {
let triggers =
get_transition_triggers("working", Some("diagnose"), "working", Some("diagnose"));
assert!(triggers.is_empty());
}
#[test]
fn test_expand_prompt_valid_exits() {
let states_config = StatesConfig::default();
let phases_config = PhasesConfig::default();
let ctx = PromptContext::new("working", None, &states_config, &phases_config);
let template = "From {{current_status}} you can go to:\n{{valid_exits}}";
let result = expand_prompt(template, &ctx);
assert!(result.contains("From working you can go to:"));
assert!(result.contains("`completed`"));
assert!(result.contains("`failed`"));
assert!(result.contains("`pending`"));
}
#[test]
fn test_expand_prompt_current_phase() {
let states_config = StatesConfig::default();
let phases_config = PhasesConfig::default();
let ctx = PromptContext::new("working", Some("implement"), &states_config, &phases_config);
let template = "Phase: {{current_phase}}";
let result = expand_prompt(template, &ctx);
assert_eq!(result, "Phase: `implement`");
let ctx = PromptContext::new("working", None, &states_config, &phases_config);
let result = expand_prompt(template, &ctx);
assert_eq!(result, "Phase: _(none)_");
}
#[test]
fn test_expand_prompt_valid_phases() {
let states_config = StatesConfig::default();
let phases_config = PhasesConfig::default();
let ctx = PromptContext::new("working", None, &states_config, &phases_config);
let template = "Phases: {{valid_phases}}";
let result = expand_prompt(template, &ctx);
assert!(result.contains("implement"));
assert!(result.contains("test"));
assert!(result.contains("review"));
}
#[test]
fn test_expand_prompt_terminal_state() {
let states_config = StatesConfig::default();
let phases_config = PhasesConfig::default();
let ctx = PromptContext::new("cancelled", None, &states_config, &phases_config);
let template = "Exits: {{valid_exits}}";
let result = expand_prompt(template, &ctx);
assert!(result.contains("no transitions available"));
}
#[test]
fn test_load_prompt_from_workflows() {
let workflows = WorkflowsConfig::default();
let prompt = load_prompt("enter~working", &workflows);
assert!(prompt.is_some());
assert!(prompt.unwrap().contains("actively working"));
let prompt = load_prompt("enter%implement", &workflows);
assert!(prompt.is_some());
assert!(prompt.unwrap().contains("Implementation"));
}
#[test]
fn test_get_transition_prompts() {
let workflows = WorkflowsConfig::default();
let prompts = get_transition_prompts("pending", None, "working", None, &workflows);
assert!(!prompts.is_empty());
assert!(prompts.iter().any(|p| p.contains("actively working")));
}
#[test]
fn test_list_available_prompts() {
let workflows = WorkflowsConfig::default();
let prompts = list_available_prompts(&workflows);
assert!(prompts.contains(&"enter~working".to_string()));
assert!(prompts.contains(&"exit~working".to_string()));
assert!(prompts.contains(&"enter%implement".to_string()));
}
}