lash_plugin_plan_mode/plan_mode/
prompt.rs1use 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}