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(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum WizardModalMode {
116 MultiStep,
118 TabbedList,
120}
121
122#[derive(Clone, Debug)]
128pub struct PlanStep {
129 pub number: usize,
130 pub description: String,
131 pub details: Option<String>,
132 pub files: Vec<String>,
133 pub completed: bool,
134}
135
136#[derive(Clone, Debug)]
138pub struct PlanPhase {
139 pub name: String,
140 pub steps: Vec<PlanStep>,
141 pub completed: bool,
142}
143
144#[derive(Clone, Debug)]
146pub struct PlanContent {
147 pub title: String,
148 pub summary: String,
149 pub file_path: Option<String>,
150 pub phases: Vec<PlanPhase>,
151 pub open_questions: Vec<String>,
152 pub raw_content: String,
153 pub total_steps: usize,
154 pub completed_steps: usize,
155}
156
157impl PlanContent {
158 pub fn from_markdown(title: String, content: &str, file_path: Option<String>) -> Self {
160 let mut phases = Vec::new();
161 let mut open_questions = Vec::new();
162 let mut current_phase: Option<PlanPhase> = None;
163 let mut total_steps = 0;
164 let mut completed_steps = 0;
165 let mut summary = String::new();
166
167 for line in content.lines() {
168 let trimmed = line.trim();
169
170 if summary.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('#') {
172 summary = trimmed.to_string();
173 continue;
174 }
175
176 if let Some(phase_name) = trimmed.strip_prefix("## ") {
178 if let Some(phase) = current_phase.take() {
179 phases.push(phase);
180 }
181 current_phase = Some(PlanPhase {
182 name: phase_name.to_string(),
183 steps: Vec::new(),
184 completed: false,
185 });
186 continue;
187 }
188
189 if trimmed == "## Open Questions" {
191 if let Some(phase) = current_phase.take() {
192 phases.push(phase);
193 }
194 continue;
195 }
196
197 if let Some(rest) = trimmed.strip_prefix("[ ] ") {
199 total_steps += 1;
200 if let Some(ref mut phase) = current_phase {
201 phase.steps.push(PlanStep {
202 number: phase.steps.len() + 1,
203 description: rest.to_string(),
204 details: None,
205 files: Vec::new(),
206 completed: false,
207 });
208 }
209 continue;
210 }
211
212 if let Some(rest) = trimmed
213 .strip_prefix("[x] ")
214 .or_else(|| trimmed.strip_prefix("[X] "))
215 {
216 total_steps += 1;
217 completed_steps += 1;
218 if let Some(ref mut phase) = current_phase {
219 phase.steps.push(PlanStep {
220 number: phase.steps.len() + 1,
221 description: rest.to_string(),
222 details: None,
223 files: Vec::new(),
224 completed: true,
225 });
226 }
227 continue;
228 }
229
230 if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains('.') {
232 total_steps += 1;
233 if let Some(ref mut phase) = current_phase {
234 let desc = trimmed.split_once('.').map(|x| x.1).unwrap_or("").trim();
235 phase.steps.push(PlanStep {
236 number: phase.steps.len() + 1,
237 description: desc.to_string(),
238 details: None,
239 files: Vec::new(),
240 completed: false,
241 });
242 }
243 continue;
244 }
245
246 if trimmed.starts_with("- (") || trimmed.starts_with("- ?") {
248 open_questions.push(trimmed.trim_start_matches("- ").to_string());
249 }
250 }
251
252 if let Some(mut phase) = current_phase.take() {
254 phase.completed = phase.steps.iter().all(|s| s.completed);
255 phases.push(phase);
256 }
257
258 for phase in &mut phases {
260 phase.completed = !phase.steps.is_empty() && phase.steps.iter().all(|s| s.completed);
261 }
262
263 Self {
264 title,
265 summary,
266 file_path,
267 phases,
268 open_questions,
269 raw_content: content.to_string(),
270 total_steps,
271 completed_steps,
272 }
273 }
274
275 #[allow(clippy::cast_sign_loss)]
277 pub fn progress_percent(&self) -> u8 {
278 if self.total_steps == 0 {
279 0
280 } else {
281 ((self.completed_steps as f32 / self.total_steps as f32) * 100.0) as u8
282 }
283 }
284}