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    /// Use for every grouped modal list so sections read the same way.
136    #[must_use]
137    pub fn group_header(title: impl Into<String>) -> Self {
138        Self {
139            title: title.into(),
140            kind: InlineItemKind::Header,
141            ..Self::default()
142        }
143    }
144
145    /// Full-width rule between option groups (approve vs deny, lists vs actions).
146    #[must_use]
147    pub fn group_divider() -> Self {
148        // Empty title is the canonical untitled divider (`is_divider_title`).
149        Self::default()
150    }
151
152    #[must_use]
153    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
154        self.subtitle = Some(subtitle.into());
155        self
156    }
157
158    #[must_use]
159    pub fn with_value(mut self, value: impl Into<String>) -> Self {
160        self.value = Some(value.into());
161        self
162    }
163
164    #[must_use]
165    pub fn with_badge(mut self, label: impl Into<String>, tone: InlineTone) -> Self {
166        self.badge = Some(label.into());
167        self.badge_tone = tone;
168        self
169    }
170
171    #[must_use]
172    pub fn with_kind(mut self, kind: InlineItemKind) -> Self {
173        self.kind = kind;
174        self
175    }
176
177    #[must_use]
178    pub fn with_indent(mut self, indent: u8) -> Self {
179        self.indent = indent;
180        self
181    }
182
183    #[must_use]
184    pub fn with_search_value(mut self, search_value: impl Into<String>) -> Self {
185        self.search_value = Some(search_value.into());
186        self
187    }
188
189    /// Legacy section-header heuristic: non-selectable rows that are not
190    /// dividers act as group headers (bold title + blank gap). Explicit
191    /// [`InlineItemKind::Hint`] rows are notes, not headers.
192    #[must_use]
193    pub fn is_header(&self) -> bool {
194        self.selection.is_none() && self.kind != InlineItemKind::Hint
195    }
196}
197
198/// A single step in a wizard modal flow.
199#[derive(Clone, Debug)]
200pub struct WizardStep {
201    /// Title displayed in the tab header.
202    pub title: String,
203    /// Question or instruction shown above the list.
204    pub question: String,
205    /// Selectable items for this step.
206    pub items: Vec<InlineListItem>,
207    /// Whether this step has been completed.
208    pub completed: bool,
209    /// The selected answer for this step (if completed).
210    pub answer: Option<InlineListSelection>,
211
212    pub allow_freeform: bool,
213    pub freeform_label: Option<String>,
214    pub freeform_placeholder: Option<String>,
215    pub freeform_default: Option<String>,
216}