vtcode_commons/ui_protocol/
types.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub enum InlineMessageKind {
6 Agent,
7 Error,
8 Info,
9 Policy,
10 Pty,
11 Tool,
12 User,
13 Warning,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SlashCommandItem {
19 pub name: String,
20 pub description: String,
21}
22
23impl SlashCommandItem {
24 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
25 Self {
26 name: name.into(),
27 description: description.into(),
28 }
29 }
30}
31
32#[derive(Clone, Debug)]
34pub struct InlineListSearchConfig {
35 pub label: String,
36 pub placeholder: Option<String>,
37}
38
39#[derive(Clone, Debug)]
41pub struct SecurePromptConfig {
42 pub label: String,
43 pub placeholder: Option<String>,
45 pub mask_input: bool,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum SessionSurface {
52 #[default]
53 Auto,
54 Alternate,
55 Inline,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct KeyboardProtocolSettings {
61 pub enabled: bool,
62 pub mode: String,
63 pub disambiguate_escape_codes: bool,
64 pub report_event_types: bool,
65 pub report_alternate_keys: bool,
66 pub report_all_keys: bool,
67}
68
69impl Default for KeyboardProtocolSettings {
70 fn default() -> Self {
71 Self {
72 enabled: true,
73 mode: "default".to_owned(),
74 disambiguate_escape_codes: true,
75 report_event_types: true,
76 report_alternate_keys: true,
77 report_all_keys: false,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum UiMode {
86 #[default]
87 Full,
88 Minimal,
89 Focused,
90}
91
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
94#[serde(rename_all = "snake_case")]
95pub enum LayoutModeOverride {
96 #[default]
97 Auto,
98 Compact,
99 Standard,
100 Wide,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum ReasoningDisplayMode {
107 Always,
108 #[default]
109 Toggle,
110 Hidden,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum EditingMode {
116 #[default]
118 Edit,
119 Plan,
121}
122
123impl EditingMode {
124 pub fn next(self) -> Self {
126 match self {
127 Self::Edit => Self::Plan,
128 Self::Plan => Self::Edit,
129 }
130 }
131
132 pub fn display_name(&self) -> &'static str {
134 match self {
135 Self::Edit => "Edit",
136 Self::Plan => "Plan",
137 }
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum WizardModalMode {
144 MultiStep,
146 TabbedList,
148}
149
150#[derive(Clone, Debug)]
156pub struct PlanStep {
157 pub number: usize,
158 pub description: String,
159 pub details: Option<String>,
160 pub files: Vec<String>,
161 pub completed: bool,
162}
163
164#[derive(Clone, Debug)]
166pub struct PlanPhase {
167 pub name: String,
168 pub steps: Vec<PlanStep>,
169 pub completed: bool,
170}
171
172#[derive(Clone, Debug)]
174pub struct PlanContent {
175 pub title: String,
176 pub summary: String,
177 pub file_path: Option<String>,
178 pub phases: Vec<PlanPhase>,
179 pub open_questions: Vec<String>,
180 pub raw_content: String,
181 pub total_steps: usize,
182 pub completed_steps: usize,
183}
184
185impl PlanContent {
186 pub fn from_markdown(title: String, content: &str, file_path: Option<String>) -> Self {
188 let mut phases = Vec::new();
189 let mut open_questions = Vec::new();
190 let mut current_phase: Option<PlanPhase> = None;
191 let mut total_steps = 0;
192 let mut completed_steps = 0;
193 let mut summary = String::new();
194
195 for line in content.lines() {
196 let trimmed = line.trim();
197
198 if summary.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('#') {
200 summary = trimmed.to_string();
201 continue;
202 }
203
204 if let Some(phase_name) = trimmed.strip_prefix("## ") {
206 if let Some(phase) = current_phase.take() {
207 phases.push(phase);
208 }
209 current_phase = Some(PlanPhase {
210 name: phase_name.to_string(),
211 steps: Vec::new(),
212 completed: false,
213 });
214 continue;
215 }
216
217 if trimmed == "## Open Questions" {
219 if let Some(phase) = current_phase.take() {
220 phases.push(phase);
221 }
222 continue;
223 }
224
225 if let Some(rest) = trimmed.strip_prefix("[ ] ") {
227 total_steps += 1;
228 if let Some(ref mut phase) = current_phase {
229 phase.steps.push(PlanStep {
230 number: phase.steps.len() + 1,
231 description: rest.to_string(),
232 details: None,
233 files: Vec::new(),
234 completed: false,
235 });
236 }
237 continue;
238 }
239
240 if let Some(rest) = trimmed
241 .strip_prefix("[x] ")
242 .or_else(|| trimmed.strip_prefix("[X] "))
243 {
244 total_steps += 1;
245 completed_steps += 1;
246 if let Some(ref mut phase) = current_phase {
247 phase.steps.push(PlanStep {
248 number: phase.steps.len() + 1,
249 description: rest.to_string(),
250 details: None,
251 files: Vec::new(),
252 completed: true,
253 });
254 }
255 continue;
256 }
257
258 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains('.') {
260 total_steps += 1;
261 if let Some(ref mut phase) = current_phase {
262 let desc = trimmed.split_once('.').map(|x| x.1).unwrap_or("").trim();
263 phase.steps.push(PlanStep {
264 number: phase.steps.len() + 1,
265 description: desc.to_string(),
266 details: None,
267 files: Vec::new(),
268 completed: false,
269 });
270 }
271 continue;
272 }
273
274 if trimmed.starts_with("- (") || trimmed.starts_with("- ?") {
276 open_questions.push(trimmed.trim_start_matches("- ").to_string());
277 }
278 }
279
280 if let Some(mut phase) = current_phase.take() {
282 phase.completed = phase.steps.iter().all(|s| s.completed);
283 phases.push(phase);
284 }
285
286 for phase in &mut phases {
288 phase.completed = !phase.steps.is_empty() && phase.steps.iter().all(|s| s.completed);
289 }
290
291 Self {
292 title,
293 summary,
294 file_path,
295 phases,
296 open_questions,
297 raw_content: content.to_string(),
298 total_steps,
299 completed_steps,
300 }
301 }
302
303 pub fn progress_percent(&self) -> u8 {
305 if self.total_steps == 0 {
306 0
307 } else {
308 ((self.completed_steps as f32 / self.total_steps as f32) * 100.0) as u8
309 }
310 }
311}