Skip to main content

vtcode_commons/ui_protocol/
types.rs

1//! Pure data types with no dependencies beyond `std`.
2
3/// Message kind tag for inline transcript lines.
4#[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/// A single slash-command entry for the suggestion palette.
17#[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/// Search configuration for a list overlay.
33#[derive(Clone, Debug)]
34pub struct InlineListSearchConfig {
35    pub label: String,
36    pub placeholder: Option<String>,
37}
38
39/// Configuration for a secure (masked) prompt input.
40#[derive(Clone, Debug)]
41pub struct SecurePromptConfig {
42    pub label: String,
43    /// Optional placeholder shown when input is empty.
44    pub placeholder: Option<String>,
45    /// Whether the input should be masked (e.g., API keys).
46    pub mask_input: bool,
47}
48
49/// Standalone surface preference for selecting inline vs alternate rendering.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum SessionSurface {
52    #[default]
53    Auto,
54    Alternate,
55    Inline,
56}
57
58/// Standalone keyboard protocol settings for terminal key event enhancements.
59#[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/// UI mode variants for quick presets.
83#[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/// Override for responsive layout detection.
93#[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/// Reasoning visibility behavior in the transcript.
104#[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/// Wizard modal behavior variant.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum WizardModalMode {
116    /// Traditional multi-step wizard behavior (Enter advances/collects answers).
117    MultiStep,
118    /// Tabbed list behavior (tabs switch categories; Enter submits immediately).
119    TabbedList,
120}
121
122// ---------------------------------------------------------------------------
123// Plan types
124// ---------------------------------------------------------------------------
125
126/// A step in an implementation plan.
127#[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/// A phase in an implementation plan (groups related steps).
137#[derive(Clone, Debug)]
138pub struct PlanPhase {
139    pub name: String,
140    pub steps: Vec<PlanStep>,
141    pub completed: bool,
142}
143
144/// Structured plan content for display in the Implementation Blueprint panel.
145#[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    /// Parse plan content from markdown.
159    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            // Extract summary from first paragraph
171            if summary.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('#') {
172                summary = trimmed.to_string();
173                continue;
174            }
175
176            // Phase headers (## Phase X: ...)
177            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            // Open questions section
190            if trimmed == "## Open Questions" {
191                if let Some(phase) = current_phase.take() {
192                    phases.push(phase);
193                }
194                continue;
195            }
196
197            // Step items ([ ] or [x] prefixed)
198            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            // Numbered steps (1. **Step 1** ...)
231            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            // Question items
247            if trimmed.starts_with("- (") || trimmed.starts_with("- ?") {
248                open_questions.push(trimmed.trim_start_matches("- ").to_string());
249            }
250        }
251
252        // Save last phase
253        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        // Update phase completion status
259        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    /// Get progress as a percentage.
276    #[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}