Add template variable injection for --include-prompt
Supports {{var}} syntax for variable substitution in included prompt files.
Currently supported variables:
- {{today}}: Current date in ISO format (YYYY-MM-DD)
Unknown variables trigger a warning and are left unchanged.
- Add template.rs module with process_template() function
- Integrate template processing into read_include_prompt()
- Add comprehensive tests for template processing
This commit is contained in:
@@ -20,6 +20,7 @@ mod task_execution;
|
|||||||
mod ui_writer_impl;
|
mod ui_writer_impl;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod g3_status;
|
mod g3_status;
|
||||||
|
mod template;
|
||||||
mod completion;
|
mod completion;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::template::process_template;
|
||||||
|
|
||||||
/// Read AGENTS.md configuration from the workspace directory.
|
/// Read AGENTS.md configuration from the workspace directory.
|
||||||
/// Returns formatted content with emoji prefix, or None if not found.
|
/// Returns formatted content with emoji prefix, or None if not found.
|
||||||
pub fn read_agents_config(workspace_dir: &Path) -> Option<String> {
|
pub fn read_agents_config(workspace_dir: &Path) -> Option<String> {
|
||||||
@@ -97,7 +99,10 @@ pub fn read_include_prompt(path: Option<&std::path::Path>) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match std::fs::read_to_string(path) {
|
match std::fs::read_to_string(path) {
|
||||||
Ok(content) => Some(format!("📎 Included Prompt (from {}):\n{}", path.display(), content)),
|
Ok(content) => {
|
||||||
|
let processed = process_template(&content);
|
||||||
|
Some(format!("📎 Included Prompt (from {}):\n{}", path.display(), processed))
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to read include prompt file {}: {}", path.display(), e);
|
tracing::error!("Failed to read include prompt file {}: {}", path.display(), e);
|
||||||
None
|
None
|
||||||
@@ -380,4 +385,23 @@ mod tests {
|
|||||||
// Cleanup
|
// Cleanup
|
||||||
let _ = std::fs::remove_file(&temp_file);
|
let _ = std::fs::remove_file(&temp_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_include_prompt_with_template_variables() {
|
||||||
|
// Create a temp file with template variables
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let temp_file = temp_dir.join("test_include_prompt_template.md");
|
||||||
|
std::fs::write(&temp_file, "Today is {{today}} and {{unknown}} stays").unwrap();
|
||||||
|
|
||||||
|
let result = read_include_prompt(Some(&temp_file));
|
||||||
|
assert!(result.is_some());
|
||||||
|
let content = result.unwrap();
|
||||||
|
|
||||||
|
// {{today}} should be replaced with a date, {{unknown}} should remain
|
||||||
|
assert!(!content.contains("{{today}}"));
|
||||||
|
assert!(content.contains("{{unknown}}"));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
let _ = std::fs::remove_file(&temp_file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
crates/g3-cli/src/template.rs
Normal file
140
crates/g3-cli/src/template.rs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
//! Template variable injection for included prompt files.
|
||||||
|
//!
|
||||||
|
//! Supports `{{var}}` syntax for variable substitution.
|
||||||
|
//! Currently supported variables:
|
||||||
|
//! - `today`: Current date in ISO format (YYYY-MM-DD)
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
/// Process template variables in the given content.
|
||||||
|
///
|
||||||
|
/// Replaces `{{var}}` patterns with their values.
|
||||||
|
/// Warns about unknown variables and leaves them unchanged.
|
||||||
|
pub fn process_template(content: &str) -> String {
|
||||||
|
// Regex to match {{variable_name}}
|
||||||
|
let re = Regex::new(r"\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}").unwrap();
|
||||||
|
|
||||||
|
// Track unknown variables to warn only once per variable
|
||||||
|
let mut unknown_vars: HashSet<String> = HashSet::new();
|
||||||
|
|
||||||
|
let result = re.replace_all(content, |caps: ®ex::Captures| {
|
||||||
|
let var_name = &caps[1];
|
||||||
|
match resolve_variable(var_name) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => {
|
||||||
|
if unknown_vars.insert(var_name.to_string()) {
|
||||||
|
tracing::warn!("Unknown template variable: {{{{{}}}}}", var_name);
|
||||||
|
}
|
||||||
|
// Leave unknown variables unchanged
|
||||||
|
caps[0].to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a template variable to its value.
|
||||||
|
fn resolve_variable(name: &str) -> Option<String> {
|
||||||
|
match name {
|
||||||
|
"today" => Some(Local::now().format("%Y-%m-%d").to_string()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_today_variable() {
|
||||||
|
let input = "Today is {{today}}";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Should contain a date in YYYY-MM-DD format
|
||||||
|
assert!(!result.contains("{{today}}"));
|
||||||
|
assert!(result.starts_with("Today is "));
|
||||||
|
|
||||||
|
// Verify date format (YYYY-MM-DD)
|
||||||
|
let date_part = &result["Today is ".len()..];
|
||||||
|
assert_eq!(date_part.len(), 10);
|
||||||
|
assert_eq!(&date_part[4..5], "-");
|
||||||
|
assert_eq!(&date_part[7..8], "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_multiple_today_variables() {
|
||||||
|
let input = "Start: {{today}}, End: {{today}}";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Both should be replaced
|
||||||
|
assert!(!result.contains("{{today}}"));
|
||||||
|
assert!(result.contains("Start: "));
|
||||||
|
assert!(result.contains(", End: "));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unknown_variable_unchanged() {
|
||||||
|
let input = "Hello {{unknown_var}}!";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Unknown variable should remain unchanged
|
||||||
|
assert_eq!(result, "Hello {{unknown_var}}!");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mixed_known_and_unknown() {
|
||||||
|
let input = "Date: {{today}}, Name: {{name}}";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// today should be replaced, name should remain
|
||||||
|
assert!(!result.contains("{{today}}"));
|
||||||
|
assert!(result.contains("{{name}}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_variables() {
|
||||||
|
let input = "No variables here";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
assert_eq!(result, "No variables here");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_braces() {
|
||||||
|
let input = "Empty {{}} braces";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Empty braces don't match the pattern, should remain unchanged
|
||||||
|
assert_eq!(result, "Empty {{}} braces");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_single_braces_ignored() {
|
||||||
|
let input = "Single {today} braces";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Single braces should not be processed
|
||||||
|
assert_eq!(result, "Single {today} braces");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable_with_underscores() {
|
||||||
|
let input = "{{my_custom_var}}";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Unknown but valid variable name, should remain unchanged
|
||||||
|
assert_eq!(result, "{{my_custom_var}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_variable_with_numbers() {
|
||||||
|
let input = "{{var123}}";
|
||||||
|
let result = process_template(input);
|
||||||
|
|
||||||
|
// Unknown but valid variable name, should remain unchanged
|
||||||
|
assert_eq!(result, "{{var123}}");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user