Skip to main content

lash_sansio/
plugin.rs

1use std::sync::Arc;
2
3use crate::{MessageRole, Part};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct PluginMessage {
8    pub role: MessageRole,
9    pub content: String,
10    #[serde(default, skip_serializing_if = "Vec::is_empty")]
11    pub parts: Vec<Part>,
12    #[serde(default, skip_serializing_if = "Vec::is_empty")]
13    pub images: Vec<Vec<u8>>,
14}
15
16impl PluginMessage {
17    pub fn text(role: MessageRole, content: impl Into<String>) -> Self {
18        Self {
19            role,
20            content: content.into(),
21            parts: Vec::new(),
22            images: Vec::new(),
23        }
24    }
25
26    pub fn first_text(&self) -> Option<&str> {
27        if !self.content.is_empty() {
28            return Some(self.content.as_str());
29        }
30        self.parts.iter().find_map(|part| {
31            matches!(part.kind, crate::PartKind::Text | crate::PartKind::Prose)
32                .then_some(part.content.as_str())
33        })
34    }
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
38pub struct PromptContributionGate {
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub tools: Vec<String>,
41    #[serde(default)]
42    pub minimum_availability: crate::ToolAvailability,
43}
44
45impl PromptContributionGate {
46    pub fn is_empty(&self) -> bool {
47        self.tools.is_empty()
48    }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
52pub struct PromptContribution {
53    pub slot: crate::PromptSlot,
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub title: Option<Arc<str>>,
56    #[serde(default)]
57    pub priority: i32,
58    #[serde(default, skip_serializing_if = "PromptContributionGate::is_empty")]
59    pub gate: PromptContributionGate,
60    pub content: Arc<str>,
61}
62
63impl PromptContribution {
64    pub fn new(
65        slot: crate::PromptSlot,
66        title: impl Into<Arc<str>>,
67        content: impl Into<Arc<str>>,
68    ) -> Self {
69        let title: Arc<str> = title.into();
70        let title = (!title.trim().is_empty()).then_some(title);
71        Self {
72            slot,
73            title,
74            priority: 0,
75            gate: PromptContributionGate {
76                tools: Vec::new(),
77                minimum_availability: crate::ToolAvailability::default(),
78            },
79            content: content.into(),
80        }
81    }
82
83    pub fn with_priority(mut self, priority: i32) -> Self {
84        self.priority = priority;
85        self
86    }
87
88    pub fn requires_tool(
89        mut self,
90        tool_name: impl Into<String>,
91        minimum_availability: crate::ToolAvailability,
92    ) -> Self {
93        self.gate = PromptContributionGate {
94            tools: vec![tool_name.into()],
95            minimum_availability,
96        };
97        self
98    }
99
100    pub fn requires_any_tool(
101        mut self,
102        tool_names: impl IntoIterator<Item = impl Into<String>>,
103        minimum_availability: crate::ToolAvailability,
104    ) -> Self {
105        self.gate = PromptContributionGate {
106            tools: tool_names.into_iter().map(Into::into).collect(),
107            minimum_availability,
108        };
109        self
110    }
111
112    pub fn intro(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
113        Self::new(crate::PromptSlot::Intro, title, content)
114    }
115
116    pub fn execution(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
117        Self::new(crate::PromptSlot::Execution, title, content)
118    }
119
120    pub fn guidance(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
121        Self::new(crate::PromptSlot::Guidance, title, content)
122    }
123
124    pub fn project_instructions(content: impl Into<Arc<str>>) -> Self {
125        Self::new(
126            crate::PromptSlot::ProjectInstructions,
127            "Project Instructions",
128            content,
129        )
130    }
131
132    pub fn runtime_context(content: impl Into<Arc<str>>) -> Self {
133        Self::new(
134            crate::PromptSlot::RuntimeContext,
135            "Runtime Context",
136            content,
137        )
138    }
139
140    pub fn environment(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
141        Self::new(crate::PromptSlot::Environment, title, content)
142    }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(tag = "kind", rename_all = "snake_case")]
147pub enum PluginSurfaceEvent {
148    ModeIndicatorUpsert {
149        key: String,
150        label: String,
151    },
152    ModeIndicatorClear {
153        key: String,
154    },
155    PanelUpsert {
156        key: String,
157        title: String,
158        content: String,
159    },
160    PanelAppend {
161        key: String,
162        content: String,
163    },
164    PanelClear {
165        key: String,
166    },
167    Status {
168        key: String,
169        label: String,
170        #[serde(default, skip_serializing_if = "Option::is_none")]
171        detail: Option<String>,
172        #[serde(default, skip_serializing_if = "Option::is_none")]
173        transient_ms: Option<u64>,
174    },
175    Custom {
176        name: String,
177        payload: serde_json::Value,
178    },
179}
180
181#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
182#[serde(rename_all = "snake_case")]
183pub enum CheckpointKind {
184    AfterWork,
185    BeforeCompletion,
186}