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}