Skip to main content

lash_plugin_plan_mode/plan_mode/
prompt.rs

1//! Plan-mode prompt surface: approval request/response types, the prompt
2//! trait, and the user-facing exit/guidance text builders.
3
4use super::*;
5
6pub(crate) fn plan_exit_next_turn_input(display: &str, note: Option<&str>) -> String {
7    if let Some(note) = note.filter(|note| !note.trim().is_empty()) {
8        format!(
9            "The user approved the plan. Execute the plan in `{display}` now — start immediately, do not ask for confirmation.\n\nUser note: {note}"
10        )
11    } else {
12        format!(
13            "The user approved the plan. Execute the plan in `{display}` now — start immediately, do not ask for confirmation."
14        )
15    }
16}
17
18pub(crate) fn plan_exit_fresh_context_input(display: &str) -> String {
19    format!("Do a full, faithful implementation of the plan found at: {display}")
20}
21
22pub(crate) fn plan_exit_confirmation_display(selection: &str, note: Option<&str>) -> String {
23    if let Some(note) = note.filter(|note| !note.trim().is_empty()) {
24        format!("{selection}\n\nNote: {note}")
25    } else {
26        selection.to_string()
27    }
28}
29
30pub(crate) fn plan_mode_guidance_message(plan_path: &Path) -> PluginMessage {
31    let display = plan_display_path(plan_path);
32    PluginMessage::text(
33        lash_core::MessageRole::System,
34        format!(
35            "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()`."
36        ),
37    )
38}
39
40pub(crate) fn plan_mode_tool_note(plan_path: Option<&Path>) -> String {
41    match plan_path {
42        Some(path) => format!(
43            "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()`.",
44            plan_display_path(path)
45        ),
46        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(),
47    }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
51pub struct PlanModePromptRequest {
52    pub question: String,
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub options: Vec<String>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub review: Option<PlanModePromptReview>,
57    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
58    pub allow_note: bool,
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62pub struct PlanModePromptReview {
63    pub title: String,
64    pub markdown: String,
65}
66
67impl PlanModePromptRequest {
68    pub fn single(question: impl Into<String>, options: Vec<String>) -> Self {
69        Self {
70            question: question.into(),
71            options,
72            review: None,
73            allow_note: false,
74        }
75    }
76
77    pub fn with_review(mut self, title: impl Into<String>, markdown: impl Into<String>) -> Self {
78        self.review = Some(PlanModePromptReview {
79            title: title.into(),
80            markdown: markdown.into(),
81        });
82        self
83    }
84
85    pub fn with_optional_note(mut self) -> Self {
86        self.allow_note = !self.options.is_empty();
87        self
88    }
89}
90
91#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
92#[serde(tag = "kind", rename_all = "snake_case")]
93pub enum PlanModePromptResponse {
94    Single {
95        selection: String,
96        #[serde(default, skip_serializing_if = "Option::is_none")]
97        note: Option<String>,
98    },
99}
100
101#[async_trait::async_trait]
102pub trait PlanModePrompt: Send + Sync {
103    async fn prompt_user(
104        &self,
105        request: PlanModePromptRequest,
106    ) -> Result<PlanModePromptResponse, PluginError>;
107}