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/// Editing mode for the agent session.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
115pub enum EditingMode {
116    /// Full tool access -- can edit files and run commands.
117    #[default]
118    Edit,
119    /// Read-only mode -- produces implementation plans without executing.
120    Plan,
121}
122
123impl EditingMode {
124    /// Cycle to the next mode: Edit -> Plan -> Edit.
125    pub fn next(self) -> Self {
126        match self {
127            Self::Edit => Self::Plan,
128            Self::Plan => Self::Edit,
129        }
130    }
131
132    /// Get display name for the mode.
133    pub fn display_name(&self) -> &'static str {
134        match self {
135            Self::Edit => "Edit",
136            Self::Plan => "Plan",
137        }
138    }
139}
140
141/// Wizard modal behavior variant.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum WizardModalMode {
144    /// Traditional multi-step wizard behavior (Enter advances/collects answers).
145    MultiStep,
146    /// Tabbed list behavior (tabs switch categories; Enter submits immediately).
147    TabbedList,
148}
149
150// ---------------------------------------------------------------------------
151// Plan types
152// ---------------------------------------------------------------------------
153
154/// A step in an implementation plan.
155#[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/// A phase in an implementation plan (groups related steps).
165#[derive(Clone, Debug)]
166pub struct PlanPhase {
167    pub name: String,
168    pub steps: Vec<PlanStep>,
169    pub completed: bool,
170}
171
172/// Structured plan content for display in the Implementation Blueprint panel.
173#[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    /// Parse plan content from markdown.
187    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            // Extract summary from first paragraph
199            if summary.is_empty() && !trimmed.is_empty() && !trimmed.starts_with('#') {
200                summary = trimmed.to_string();
201                continue;
202            }
203
204            // Phase headers (## Phase X: ...)
205            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            // Open questions section
218            if trimmed == "## Open Questions" {
219                if let Some(phase) = current_phase.take() {
220                    phases.push(phase);
221                }
222                continue;
223            }
224
225            // Step items ([ ] or [x] prefixed)
226            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            // Numbered steps (1. **Step 1** ...)
259            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            // Question items
275            if trimmed.starts_with("- (") || trimmed.starts_with("- ?") {
276                open_questions.push(trimmed.trim_start_matches("- ").to_string());
277            }
278        }
279
280        // Save last phase
281        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        // Update phase completion status
287        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    /// Get progress as a percentage.
304    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}