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    SessionLimitIncrease(usize),
55    RewindCheckpoint(usize),
56    RewindAction(RewindAction),
57
58    /// Selection shape used by legacy tabbed HITL flows.
59    AskUserChoice {
60        tab_id: String,
61        choice_id: String,
62        text: Option<String>,
63    },
64
65    /// Selection returned from the `request_user_input` HITL tool.
66    RequestUserInputAnswer {
67        question_id: String,
68        selected: Vec<String>,
69        other: Option<String>,
70    },
71
72    /// Plan confirmation dialog result (human-in-the-loop flow).
73    PlanApprovalExecute,
74    /// Execute the approved plan after clearing transient context.
75    PlanApprovalFreshContext,
76    /// Return to planning to edit the plan file.
77    PlanApprovalEditPlan,
78    /// Return to planning to discuss and revise the plan in chat.
79    PlanApprovalDiscuss,
80    /// Auto-accept all future plans in this session.
81    PlanApprovalAutoAccept,
82    /// Hand off to the build primary agent and execute the plan.
83    PlanApprovalSwitchBuild,
84    /// Hand off to the auto primary agent (auto-execute with per-step HITL).
85    PlanApprovalSwitchAuto,
86}
87
88/// Role of a list row, used by the renderer to pick emphasis and spacing.
89///
90/// Defaults to [`InlineItemKind::Item`] so existing callers keep today's look
91/// until they opt into a more specific kind.
92#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub enum InlineItemKind {
94    /// Ordinary selectable (or non-selectable) row.
95    #[default]
96    Item,
97    /// Non-selectable section header (bold title, blank gap above).
98    Header,
99    /// Config key row: `title` is the label, `value` is the live setting value.
100    Setting,
101    /// Imperative row (Reset, Reload, Back, Pick model, …).
102    Action,
103    /// Non-selectable note (dimmed).
104    Hint,
105}
106
107/// A selectable item inside a list overlay.
108#[derive(Clone, Debug, Default)]
109pub struct InlineListItem {
110    pub title: String,
111    /// Secondary description / metadata. Rendered dimmed; do **not** pack the
112    /// live value here — use [`Self::value`] so the renderer can accent it.
113    pub subtitle: Option<String>,
114    /// Short trailing/leading label (provider, "On", "Edit", …).
115    pub badge: Option<String>,
116    pub indent: u8,
117    pub selection: Option<InlineListSelection>,
118    pub search_value: Option<String>,
119    /// Live value for setting rows (accent-styled by the renderer).
120    pub value: Option<String>,
121    /// Semantic tone for `badge` (and for `value` emphasis on setting rows).
122    pub badge_tone: InlineTone,
123    /// Row role controlling title emphasis and spacing.
124    pub kind: InlineItemKind,
125}
126
127impl InlineListItem {
128    /// Minimal selectable row: `title` + `selection`, everything else default.
129    #[must_use]
130    pub fn new(title: impl Into<String>, selection: Option<InlineListSelection>) -> Self {
131        Self { title: title.into(), selection, ..Self::default() }
132    }
133
134    /// Shared group header: bold title, blank spacing above and below.
135    /// Renders as a labeled heading (`» Title`) so sections
136    /// stay distinct from plain items and full-width dividers.
137    /// Use for every grouped modal list so sections read the same way.
138    #[must_use]
139    pub fn group_header(title: impl Into<String>) -> Self {
140        Self {
141            title: title.into(),
142            kind: InlineItemKind::Header,
143            ..Self::default()
144        }
145    }
146
147    /// Full-width rule between option groups (approve vs deny, lists vs actions).
148    #[must_use]
149    pub fn group_divider() -> Self {
150        // Empty title is the canonical untitled divider (`is_divider_title`).
151        Self::default()
152    }
153
154    #[must_use]
155    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
156        self.subtitle = Some(subtitle.into());
157        self
158    }
159
160    #[must_use]
161    pub fn with_value(mut self, value: impl Into<String>) -> Self {
162        self.value = Some(value.into());
163        self
164    }
165
166    #[must_use]
167    pub fn with_badge(mut self, label: impl Into<String>, tone: InlineTone) -> Self {
168        self.badge = Some(label.into());
169        self.badge_tone = tone;
170        self
171    }
172
173    #[must_use]
174    pub fn with_kind(mut self, kind: InlineItemKind) -> Self {
175        self.kind = kind;
176        self
177    }
178
179    #[must_use]
180    pub fn with_indent(mut self, indent: u8) -> Self {
181        self.indent = indent;
182        self
183    }
184
185    #[must_use]
186    pub fn with_search_value(mut self, search_value: impl Into<String>) -> Self {
187        self.search_value = Some(search_value.into());
188        self
189    }
190
191    /// Legacy section-header heuristic: non-selectable rows that are not
192    /// dividers act as group headers (bold title + blank gap). Explicit
193    /// [`InlineItemKind::Hint`] rows are notes, not headers.
194    #[must_use]
195    pub fn is_header(&self) -> bool {
196        self.selection.is_none() && self.kind != InlineItemKind::Hint
197    }
198}
199
200/// A single step in a wizard modal flow.
201#[derive(Clone, Debug)]
202pub struct WizardStep {
203    /// Title displayed in the tab header.
204    pub title: String,
205    /// Question or instruction shown above the list.
206    pub question: String,
207    /// Selectable items for this step.
208    pub items: Vec<InlineListItem>,
209    /// Whether this step has been completed.
210    pub completed: bool,
211    /// The selected answer for this step (if completed).
212    pub answer: Option<InlineListSelection>,
213
214    pub allow_freeform: bool,
215    pub freeform_label: Option<String>,
216    pub freeform_placeholder: Option<String>,
217    pub freeform_default: Option<String>,
218}