use vtcode_core::llm::provider as uni;
use vtcode_core::planning;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PlanningIntent {
ExitAndImplement,
StayInPlanning,
None,
}
pub(crate) fn detect_planning_intent(text: &str, assistant_prompted_implementation: bool) -> PlanningIntent {
let normalized = planning::normalize_plan_intent(text);
let trimmed = normalized.trim();
if planning::matches_stay_intent(&normalized) {
return PlanningIntent::StayInPlanning;
}
if planning::matches_exit_intent(&normalized) {
return PlanningIntent::ExitAndImplement;
}
if assistant_prompted_implementation && is_short_confirmation(trimmed) {
return PlanningIntent::ExitAndImplement;
}
PlanningIntent::None
}
pub(crate) fn detect_enter_planning_intent(text: &str) -> bool {
let trimmed_raw = text.trim();
if trimmed_raw == "/plan" || trimmed_raw.to_ascii_lowercase().starts_with("/plan ") {
return true;
}
let normalized = planning::normalize_plan_intent(text);
planning::matches_enter_intent(&normalized)
}
pub(crate) fn assistant_recently_prompted_implementation(working_history: &[uni::Message]) -> bool {
let Some(last_user_index) = working_history.iter().rposition(|msg| msg.role == uni::MessageRole::User) else {
return false;
};
let Some(last_assistant_msg) = working_history[..last_user_index]
.iter()
.rev()
.find(|msg| msg.role == uni::MessageRole::Assistant)
else {
return false;
};
let assistant_text = planning::normalize_plan_intent(&last_assistant_msg.content.as_text());
planning::contains_implementation_cue(&assistant_text)
}
fn is_short_confirmation(trimmed: &str) -> bool {
let confirmation_tokens = [
"yes",
"y",
"ok",
"okay",
"continue",
"go",
"go ahead",
"proceed",
"start",
"start now",
"begin",
"begin now",
"let s start",
"lets start",
"sounds good",
"do it",
];
confirmation_tokens.contains(&trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_implement_as_exit_intent() {
assert_eq!(detect_planning_intent("implement", false), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("Implement the plan.", false), PlanningIntent::ExitAndImplement);
}
#[test]
fn detects_stay_in_planning_as_higher_priority() {
assert_eq!(
detect_planning_intent("Do not implement yet; keep planning.", false),
PlanningIntent::StayInPlanning
);
assert_eq!(
detect_planning_intent("Stay in planning workflow and don't implement.", false),
PlanningIntent::StayInPlanning
);
}
#[test]
fn detects_short_confirmation_with_context() {
assert_eq!(detect_planning_intent("yes", true), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("continue", true), PlanningIntent::ExitAndImplement);
}
#[test]
fn short_confirmation_without_context_is_none() {
assert_eq!(
detect_planning_intent("yes", false),
PlanningIntent::ExitAndImplement );
assert_eq!(detect_planning_intent("continue", false), PlanningIntent::None);
}
#[test]
fn detects_approve_as_exit_intent() {
assert_eq!(detect_planning_intent("approve", false), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("approved", false), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("approve the plan", false), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("lgtm", false), PlanningIntent::ExitAndImplement);
assert_eq!(detect_planning_intent("looks good, let's go", false), PlanningIntent::ExitAndImplement);
}
#[test]
fn disapprove_is_not_exit_intent() {
assert_eq!(detect_planning_intent("I disapprove, keep planning", false), PlanningIntent::StayInPlanning);
}
#[test]
fn detects_enter_planning_intent() {
assert!(detect_enter_planning_intent("make a plan for this"));
assert!(detect_enter_planning_intent("/plan"));
assert!(detect_enter_planning_intent("before implementing, create a plan"));
}
#[test]
fn non_intent_text_returns_none() {
assert_eq!(detect_planning_intent("The implementation details are unclear.", false), PlanningIntent::None);
}
#[test]
fn assistant_prompted_implementation_detects_cues() {
let history = vec![
uni::Message::assistant("Implement this plan?".to_string()),
uni::Message::user("yes".to_string()),
];
assert!(assistant_recently_prompted_implementation(&history));
}
#[test]
fn assistant_prompted_implementation_requires_cue() {
let history = vec![
uni::Message::assistant("Continue planning and expand the risks section.".to_string()),
uni::Message::user("yes".to_string()),
];
assert!(!assistant_recently_prompted_implementation(&history));
}
}