Skip to main content

vtcode_commons/ui_protocol/
selection.rs

1//! List selection and wizard step types.
2
3use super::style::InlineTone;
4
5/// Rewind action choices for the rewind overlay.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum RewindAction {
8    RestoreBoth,
9    RestoreConversation,
10    RestoreCode,
11    SummarizeFromHere,
12    NeverMind,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum OpenAIServiceTierChoice {
17    ProjectDefault,
18    Flex,
19    Priority,
20    Ultrafast,
21}
22
23/// Selection value returned from a list or wizard overlay.
24///
25/// The `Reasoning` variant carries a `String` reasoning-effort level rather
26/// than a typed enum so that this type stays free of config-crate dependencies.
27/// Callers convert to/from their local `ReasoningEffortLevel` as needed.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum InlineListSelection {
30    Model(usize),
31    DynamicModel(usize),
32    CustomProvider(usize),
33    RefreshDynamicModels,
34    Reasoning(String),
35    DisableReasoning,
36    OpenAIServiceTier(OpenAIServiceTierChoice),
37    CustomModel,
38    Theme(String),
39    Session(String),
40    SessionForkMode {
41        session_id: String,
42        summarize: bool,
43    },
44    ConfigAction(String),
45    SlashCommand(String),
46    ToolApproval(bool),
47    ToolApprovalDenyOnce,
48    ToolApprovalSession,
49    ToolApprovalPermanent,
50    ToolApprovalEnable,
51    FileConflictReload,
52    FileConflictViewDiff,
53    FileConflictAbort,
54    RewindCheckpoint(usize),
55    RewindAction(RewindAction),
56
57    /// Selection shape used by legacy tabbed HITL flows.
58    AskUserChoice {
59        tab_id: String,
60        choice_id: String,
61        text: Option<String>,
62    },
63
64    /// Selection returned from the `request_user_input` HITL tool.
65    RequestUserInputAnswer {
66        question_id: String,
67        selected: Vec<String>,
68        other: Option<String>,
69    },
70
71    /// Plan confirmation dialog result (human-in-the-loop flow).
72    PlanApprovalExecute,
73    /// Execute the approved plan after clearing transient context.
74    PlanApprovalFreshContext,
75    /// Return to planning to edit the plan file.
76    PlanApprovalEditPlan,
77    /// Return to planning to discuss and revise the plan in chat.
78    PlanApprovalDiscuss,
79    /// Auto-accept all future plans in this session.
80    PlanApprovalAutoAccept,
81    /// Hand off to the build primary agent and execute the plan.
82    PlanApprovalSwitchBuild,
83    /// Hand off to the auto primary agent (auto-execute with per-step HITL).
84    PlanApprovalSwitchAuto,
85}
86
87/// Role of a list row, used by the renderer to pick emphasis and spacing.
88///
89/// Defaults to [`InlineItemKind::Item`] so existing callers keep today's look
90/// until they opt into a more specific kind.
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum InlineItemKind {
93    /// Ordinary selectable (or non-selectable) row.
94    #[default]
95    Item,
96    /// Non-selectable section header (bold title, blank gap above).
97    Header,
98    /// Config key row: `title` is the label, `value` is the live setting value.
99    Setting,
100    /// Imperative row (Reset, Reload, Back, Pick model, …).
101    Action,
102    /// Non-selectable note (dimmed).
103    Hint,
104}
105
106/// A selectable item inside a list overlay.
107#[derive(Clone, Debug, Default)]
108pub struct InlineListItem {
109    pub title: String,
110    /// Secondary description / metadata. Rendered dimmed; do **not** pack the
111    /// live value here — use [`Self::value`] so the renderer can accent it.
112    pub subtitle: Option<String>,
113    /// Short trailing/leading label (provider, "On", "Edit", …).
114    pub badge: Option<String>,
115    pub indent: u8,
116    pub selection: Option<InlineListSelection>,
117    pub search_value: Option<String>,
118    /// Live value for setting rows (accent-styled by the renderer).
119    pub value: Option<String>,
120    /// Semantic tone for `badge` (and for `value` emphasis on setting rows).
121    pub badge_tone: InlineTone,
122    /// Row role controlling title emphasis and spacing.
123    pub kind: InlineItemKind,
124}
125
126impl InlineListItem {
127    /// Minimal selectable row: `title` + `selection`, everything else default.
128    #[must_use]
129    pub fn new(title: impl Into<String>, selection: Option<InlineListSelection>) -> Self {
130        Self { title: title.into(), selection, ..Self::default() }
131    }
132
133    /// Shared group header: bold title, blank spacing above and below.
134    /// Renders as a labeled heading (`» Title`) so sections
135    /// stay distinct from plain items and full-width dividers.
136    /// Use for every grouped modal list so sections read the same way.
137    #[must_use]
138    pub fn group_header(title: impl Into<String>) -> Self {
139        Self {
140            title: title.into(),
141            kind: InlineItemKind::Header,
142            ..Self::default()
143        }
144    }
145
146    /// Full-width rule between option groups (approve vs deny, lists vs actions).
147    #[must_use]
148    pub fn group_divider() -> Self {
149        // Empty title is the canonical untitled divider (`is_divider_title`).
150        Self::default()
151    }
152
153    #[must_use]
154    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
155        self.subtitle = Some(subtitle.into());
156        self
157    }
158
159    #[must_use]
160    pub fn with_value(mut self, value: impl Into<String>) -> Self {
161        self.value = Some(value.into());
162        self
163    }
164
165    #[must_use]
166    pub fn with_badge(mut self, label: impl Into<String>, tone: InlineTone) -> Self {
167        self.badge = Some(label.into());
168        self.badge_tone = tone;
169        self
170    }
171
172    #[must_use]
173    pub fn with_kind(mut self, kind: InlineItemKind) -> Self {
174        self.kind = kind;
175        self
176    }
177
178    #[must_use]
179    pub fn with_indent(mut self, indent: u8) -> Self {
180        self.indent = indent;
181        self
182    }
183
184    #[must_use]
185    pub fn with_search_value(mut self, search_value: impl Into<String>) -> Self {
186        self.search_value = Some(search_value.into());
187        self
188    }
189
190    /// Legacy section-header heuristic: non-selectable rows that are not
191    /// dividers act as group headers (bold title + blank gap). Explicit
192    /// [`InlineItemKind::Hint`] rows are notes, not headers.
193    #[must_use]
194    pub fn is_header(&self) -> bool {
195        self.selection.is_none() && self.kind != InlineItemKind::Hint
196    }
197}
198
199/// A single step in a wizard modal flow.
200#[derive(Clone, Debug)]
201pub struct WizardStep {
202    /// Title displayed in the tab header.
203    pub title: String,
204    /// Question or instruction shown above the list.
205    pub question: String,
206    /// Selectable items for this step.
207    pub items: Vec<InlineListItem>,
208    /// Whether this step has been completed.
209    pub completed: bool,
210    /// The selected answer for this step (if completed).
211    pub answer: Option<InlineListSelection>,
212
213    pub allow_freeform: bool,
214    pub freeform_label: Option<String>,
215    pub freeform_placeholder: Option<String>,
216    pub freeform_default: Option<String>,
217}