use super::*;
pub(crate) fn plan_exit_next_turn_input(display: &str, note: Option<&str>) -> String {
if let Some(note) = note.filter(|note| !note.trim().is_empty()) {
format!(
"The user approved the plan. Execute the plan in `{display}` now — start immediately, do not ask for confirmation.\n\nUser note: {note}"
)
} else {
format!(
"The user approved the plan. Execute the plan in `{display}` now — start immediately, do not ask for confirmation."
)
}
}
pub(crate) fn plan_exit_fresh_context_input(display: &str) -> String {
format!("Do a full, faithful implementation of the plan found at: {display}")
}
pub(crate) fn plan_exit_confirmation_display(selection: &str, note: Option<&str>) -> String {
if let Some(note) = note.filter(|note| !note.trim().is_empty()) {
format!("{selection}\n\nNote: {note}")
} else {
selection.to_string()
}
}
pub(crate) fn plan_mode_guidance_message(plan_path: &Path) -> PluginMessage {
let display = plan_display_path(plan_path);
PluginMessage::text(
lash_core::MessageRole::System,
format!(
"Plan mode: use `{display}` as the single source of truth. Read/search/list, web, and `ask(...)` as needed, and update only that file with `apply_patch`. Do not present the plan with snippets, showcases, or prose checklists; the host can surface the file path while planning. When the plan is ready for review, call `plan_exit()`."
),
)
}
pub(crate) fn plan_mode_tool_note(plan_path: Option<&Path>) -> String {
match plan_path {
Some(path) => format!(
"Plan mode tools: read/search/list, web search/fetch, `ask`, `apply_patch` for `{}`, `plan_exit()`. The host can surface the plan file path; full review happens in `plan_exit()`.",
plan_display_path(path)
),
None => "Plan mode tools: read/search/list, web search/fetch, `ask`, plan-file `apply_patch`, `plan_exit()`. The host can surface the plan file path; full review happens in `plan_exit()`.".to_string(),
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PlanModePromptRequest {
pub question: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub review: Option<PlanModePromptReview>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub allow_note: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PlanModePromptReview {
pub title: String,
pub markdown: String,
}
impl PlanModePromptRequest {
pub fn single(question: impl Into<String>, options: Vec<String>) -> Self {
Self {
question: question.into(),
options,
review: None,
allow_note: false,
}
}
pub fn with_review(mut self, title: impl Into<String>, markdown: impl Into<String>) -> Self {
self.review = Some(PlanModePromptReview {
title: title.into(),
markdown: markdown.into(),
});
self
}
pub fn with_optional_note(mut self) -> Self {
self.allow_note = !self.options.is_empty();
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PlanModePromptResponse {
Single {
selection: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
},
}
#[async_trait::async_trait]
pub trait PlanModePrompt: Send + Sync {
async fn prompt_user(
&self,
request: PlanModePromptRequest,
) -> Result<PlanModePromptResponse, PluginError>;
}