use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::{debug, info};
use super::piece::Piece;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum InteractiveMode {
#[default]
Assistant,
Persona,
Quiet,
Passthrough,
}
impl std::fmt::Display for InteractiveMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Assistant => write!(f, "assistant"),
Self::Persona => write!(f, "persona"),
Self::Quiet => write!(f, "quiet"),
Self::Passthrough => write!(f, "passthrough"),
}
}
}
impl InteractiveMode {
pub fn description(&self) -> &'static str {
match self {
Self::Assistant => "AI asks clarifying questions before generating task instructions",
Self::Persona => {
"Conversation with the first movement's persona (uses its system prompt and tools)"
}
Self::Quiet => "Generates task instructions without asking questions (best-effort)",
Self::Passthrough => "Passes user input directly as task text without AI processing",
}
}
pub fn all() -> Vec<Self> {
vec![
Self::Assistant,
Self::Persona,
Self::Quiet,
Self::Passthrough,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InteractiveConfig {
pub mode: InteractiveMode,
#[serde(default = "default_max_rounds")]
pub max_clarification_rounds: u32,
#[serde(default = "default_true")]
pub show_piece_selection: bool,
#[serde(default)]
pub default_piece: Option<String>,
}
fn default_max_rounds() -> u32 {
5
}
fn default_true() -> bool {
true
}
impl Default for InteractiveConfig {
fn default() -> Self {
Self {
mode: InteractiveMode::default(),
max_clarification_rounds: default_max_rounds(),
show_piece_selection: true,
default_piece: None,
}
}
}
#[derive(Debug, Clone)]
pub struct InteractiveSession {
pub selected_piece: Option<String>,
pub mode: InteractiveMode,
pub user_inputs: Vec<String>,
pub clarifications: Vec<Clarification>,
pub task_text: Option<String>,
pub variables: HashMap<String, String>,
pub ready: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Clarification {
pub question: String,
pub answer: Option<String>,
pub options: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum InteractiveAction {
AskQuestion(Clarification),
ShowMessage(String),
Execute(String),
Exit,
}
impl InteractiveSession {
pub fn new(mode: InteractiveMode) -> Self {
Self {
selected_piece: None,
mode,
user_inputs: Vec::new(),
clarifications: Vec::new(),
task_text: None,
variables: HashMap::new(),
ready: false,
}
}
pub fn select_piece(&mut self, piece_name: &str) {
self.selected_piece = Some(piece_name.to_string());
info!("Selected piece: {}", piece_name);
}
pub fn process_input(
&mut self,
input: &str,
piece: Option<&Piece>,
) -> Result<InteractiveAction> {
if let Some(action) = self.handle_command(input)? {
return Ok(action);
}
self.user_inputs.push(input.to_string());
match self.mode {
InteractiveMode::Assistant => self.process_assistant(input, piece),
InteractiveMode::Persona => self.process_persona(input, piece),
InteractiveMode::Quiet => self.process_quiet(input),
InteractiveMode::Passthrough => self.process_passthrough(input),
}
}
fn handle_command(&mut self, input: &str) -> Result<Option<InteractiveAction>> {
let trimmed = input.trim();
if trimmed == "/quit" || trimmed == "/exit" {
return Ok(Some(InteractiveAction::Exit));
}
if trimmed == "/go" {
if let Some(ref task) = self.task_text {
self.ready = true;
return Ok(Some(InteractiveAction::Execute(task.clone())));
}
let combined = self.user_inputs.join("\n");
self.task_text = Some(combined.clone());
self.ready = true;
return Ok(Some(InteractiveAction::Execute(combined)));
}
if let Some(task) = trimmed.strip_prefix("/play ") {
self.task_text = Some(task.to_string());
self.ready = true;
return Ok(Some(InteractiveAction::Execute(task.to_string())));
}
if let Some(mode_str) = trimmed.strip_prefix("/mode ") {
match mode_str.trim() {
"assistant" => self.mode = InteractiveMode::Assistant,
"persona" => self.mode = InteractiveMode::Persona,
"quiet" => self.mode = InteractiveMode::Quiet,
"passthrough" => self.mode = InteractiveMode::Passthrough,
other => {
return Ok(Some(InteractiveAction::ShowMessage(format!(
"Unknown mode: '{}'. Available: assistant, persona, quiet, passthrough",
other
))));
}
}
return Ok(Some(InteractiveAction::ShowMessage(format!(
"Switched to {} mode",
self.mode
))));
}
Ok(None)
}
fn process_assistant(
&mut self,
input: &str,
_piece: Option<&Piece>,
) -> Result<InteractiveAction> {
if let Some(last_clarification) = self.clarifications.last_mut()
&& last_clarification.answer.is_none()
{
last_clarification.answer = Some(input.to_string());
debug!("Recorded answer for: {}", last_clarification.question);
}
let context = self.build_context();
if self.clarifications.len() >= 3 || self.has_sufficient_context() {
let task = self.generate_task_from_context(&context);
self.task_text = Some(task.clone());
return Ok(InteractiveAction::ShowMessage(format!(
"Generated task:\n{}\n\nType /go to execute or continue refining.",
task
)));
}
let question = self.generate_clarification(&context);
let clarification = Clarification {
question: question.clone(),
answer: None,
options: vec![],
};
self.clarifications.push(clarification.clone());
Ok(InteractiveAction::AskQuestion(clarification))
}
fn process_persona(&mut self, input: &str, piece: Option<&Piece>) -> Result<InteractiveAction> {
let persona_name = piece
.and_then(|p| p.get_movement(&p.initial_movement))
.and_then(|m| m.persona.as_deref())
.unwrap_or("assistant");
let response = format!(
"[{}] I understand your request: \"{}\". \
I'll incorporate this into the workflow. Type /go when ready to execute.",
persona_name, input
);
let current = self.task_text.clone().unwrap_or_default();
let updated = if current.is_empty() {
input.to_string()
} else {
format!("{}\n{}", current, input)
};
self.task_text = Some(updated);
Ok(InteractiveAction::ShowMessage(response))
}
fn process_quiet(&mut self, input: &str) -> Result<InteractiveAction> {
let enhanced = format!(
"Task: {}\n\nPlease complete this task following best practices.",
input
);
self.task_text = Some(enhanced.clone());
self.ready = true;
Ok(InteractiveAction::Execute(enhanced))
}
fn process_passthrough(&mut self, input: &str) -> Result<InteractiveAction> {
self.task_text = Some(input.to_string());
self.ready = true;
Ok(InteractiveAction::Execute(input.to_string()))
}
fn build_context(&self) -> String {
let mut parts = Vec::new();
for input in &self.user_inputs {
parts.push(format!("User: {}", input));
}
for clarification in &self.clarifications {
if let Some(ref answer) = clarification.answer {
parts.push(format!("Q: {}\nA: {}", clarification.question, answer));
}
}
parts.join("\n")
}
fn has_sufficient_context(&self) -> bool {
let total_len: usize = self.user_inputs.iter().map(|s| s.len()).sum();
total_len > 200
}
fn generate_clarification(&self, context: &str) -> String {
let questions = [
"What specific outcome are you looking for?",
"Are there any constraints or requirements I should be aware of?",
"Should I prioritize any particular aspect (speed, quality, security)?",
"Are there any files or modules that should NOT be modified?",
"What testing approach would you prefer?",
];
let index = self.clarifications.len() % questions.len();
let _context = context; questions[index].to_string()
}
fn generate_task_from_context(&self, context: &str) -> String {
let mut task_parts = Vec::new();
task_parts.push("Task Summary:".to_string());
for (i, input) in self.user_inputs.iter().enumerate() {
if i == 0 {
task_parts.push(format!("Primary goal: {}", input));
} else {
task_parts.push(format!("- {}", input));
}
}
let answered: Vec<&Clarification> = self
.clarifications
.iter()
.filter(|c| c.answer.is_some())
.collect();
if !answered.is_empty() {
task_parts.push("\nAdditional context:".to_string());
for c in answered {
if let Some(ref answer) = c.answer {
task_parts.push(format!("- {} → {}", c.question, answer));
}
}
}
let _context = context; task_parts.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_interactive_modes() {
let modes = InteractiveMode::all();
assert_eq!(modes.len(), 4);
assert_eq!(InteractiveMode::default(), InteractiveMode::Assistant);
}
#[test]
fn test_mode_display() {
assert_eq!(InteractiveMode::Assistant.to_string(), "assistant");
assert_eq!(InteractiveMode::Persona.to_string(), "persona");
assert_eq!(InteractiveMode::Quiet.to_string(), "quiet");
assert_eq!(InteractiveMode::Passthrough.to_string(), "passthrough");
}
#[test]
fn test_passthrough_mode() {
let mut session = InteractiveSession::new(InteractiveMode::Passthrough);
let result = session.process_input("Fix the login bug", None).unwrap();
match result {
InteractiveAction::Execute(task) => {
assert_eq!(task, "Fix the login bug");
}
_ => panic!("Expected Execute action"),
}
assert!(session.ready);
}
#[test]
fn test_quiet_mode() {
let mut session = InteractiveSession::new(InteractiveMode::Quiet);
let result = session.process_input("Add unit tests", None).unwrap();
match result {
InteractiveAction::Execute(task) => {
assert!(task.contains("Add unit tests"));
}
_ => panic!("Expected Execute action"),
}
assert!(session.ready);
}
#[test]
fn test_assistant_mode_asks_questions() {
let mut session = InteractiveSession::new(InteractiveMode::Assistant);
let result = session
.process_input("Refactor the auth module", None)
.unwrap();
match result {
InteractiveAction::AskQuestion(q) => {
assert!(!q.question.is_empty());
}
_ => panic!("Expected AskQuestion action"),
}
assert!(!session.ready);
}
#[test]
fn test_go_command() {
let mut session = InteractiveSession::new(InteractiveMode::Assistant);
session.process_input("Build a REST API", None).unwrap();
let result = session.process_input("/go", None).unwrap();
match result {
InteractiveAction::Execute(task) => {
assert!(task.contains("Build a REST API"));
}
_ => panic!("Expected Execute action"),
}
assert!(session.ready);
}
#[test]
fn test_play_command() {
let mut session = InteractiveSession::new(InteractiveMode::Assistant);
let result = session
.process_input("/play Create a login form", None)
.unwrap();
match result {
InteractiveAction::Execute(task) => {
assert_eq!(task, "Create a login form");
}
_ => panic!("Expected Execute action"),
}
}
#[test]
fn test_mode_switch() {
let mut session = InteractiveSession::new(InteractiveMode::Assistant);
let result = session.process_input("/mode quiet", None).unwrap();
match result {
InteractiveAction::ShowMessage(msg) => {
assert!(msg.contains("quiet"));
}
_ => panic!("Expected ShowMessage action"),
}
assert_eq!(session.mode, InteractiveMode::Quiet);
}
#[test]
fn test_quit_command() {
let mut session = InteractiveSession::new(InteractiveMode::Assistant);
let result = session.process_input("/quit", None).unwrap();
match result {
InteractiveAction::Exit => {}
_ => panic!("Expected Exit action"),
}
}
#[test]
fn test_persona_mode() {
let mut session = InteractiveSession::new(InteractiveMode::Persona);
let result = session
.process_input("Optimize the database queries", None)
.unwrap();
match result {
InteractiveAction::ShowMessage(msg) => {
assert!(msg.contains("Optimize the database queries"));
}
_ => panic!("Expected ShowMessage action"),
}
assert!(session.task_text.is_some());
}
#[test]
fn test_interactive_config_default() {
let config = InteractiveConfig::default();
assert_eq!(config.mode, InteractiveMode::Assistant);
assert_eq!(config.max_clarification_rounds, 5);
assert!(config.show_piece_selection);
assert!(config.default_piece.is_none());
}
}