1pub use vtcode_commons::ui_protocol::{InlineItemKind, InlineListItem, InlineListSelection, InlineStatus, InlineTone};
11
12#[must_use]
14pub fn group_header(title: impl Into<String>) -> InlineListItem {
15 InlineListItem::group_header(title)
16}
17
18#[must_use]
20pub fn group_divider() -> InlineListItem {
21 InlineListItem::group_divider()
22}
23
24#[must_use]
26pub fn hint(text: impl Into<String>) -> InlineListItem {
27 InlineListItem {
28 title: text.into(),
29 kind: InlineItemKind::Hint,
30 ..InlineListItem::default()
31 }
32}
33
34#[must_use]
39pub fn setting(
40 title: impl Into<String>,
41 value: Option<String>,
42 description: Option<String>,
43 selection: Option<InlineListSelection>,
44) -> InlineListItem {
45 InlineListItem {
46 title: title.into(),
47 value,
48 subtitle: description,
49 selection,
50 kind: InlineItemKind::Setting,
51 badge_tone: InlineTone::Accent,
52 ..InlineListItem::default()
53 }
54}
55
56#[must_use]
58pub fn action(
59 title: impl Into<String>,
60 description: impl Into<String>,
61 badge: Option<String>,
62 tone: InlineTone,
63 selection: Option<InlineListSelection>,
64) -> InlineListItem {
65 let title = title.into();
66 let description = description.into();
67 let search_value = format!("{title} {description}").to_ascii_lowercase();
68 InlineListItem {
69 subtitle: (!description.trim().is_empty()).then_some(description),
70 title,
71 badge,
72 selection,
73 kind: InlineItemKind::Action,
74 badge_tone: tone,
75 search_value: Some(search_value),
76 ..InlineListItem::default()
77 }
78}
79
80#[must_use]
82pub fn choice(
83 title: impl Into<String>,
84 description: Option<String>,
85 selection: Option<InlineListSelection>,
86) -> InlineListItem {
87 let title = title.into();
88 let search_value = format!("{title} {}", description.clone().unwrap_or_default()).to_ascii_lowercase();
89 InlineListItem {
90 subtitle: description,
91 title,
92 selection,
93 kind: InlineItemKind::Item,
94 search_value: Some(search_value),
95 ..InlineListItem::default()
96 }
97}
98
99#[must_use]
101pub fn current_choice(
102 title: impl Into<String>,
103 description: Option<String>,
104 selection: Option<InlineListSelection>,
105) -> InlineListItem {
106 choice(title, description, selection).with_badge("Current", InlineTone::Current)
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn group_header_is_non_selectable_header() {
115 let row = group_header("Anthropic");
116 assert!(row.selection.is_none());
117 assert_eq!(row.kind, InlineItemKind::Header);
118 assert!(row.is_header());
119 }
120
121 #[test]
122 fn setting_carries_value_and_accent_tone() {
123 let row = setting("Theme", Some("mono".into()), Some("ANSI theme".into()), None);
124 assert_eq!(row.value.as_deref(), Some("mono"));
125 assert_eq!(row.subtitle.as_deref(), Some("ANSI theme"));
126 assert_eq!(row.kind, InlineItemKind::Setting);
127 assert_eq!(row.badge_tone, InlineTone::Accent);
128 }
129
130 #[test]
131 fn action_uses_explicit_tone_and_skips_empty_description() {
132 let row = action("Reset", "", Some("Destructive".to_string()), InlineTone::Danger, None);
133 assert_eq!(row.kind, InlineItemKind::Action);
134 assert_eq!(row.badge_tone, InlineTone::Danger);
135 assert!(row.subtitle.is_none());
136 }
137
138 #[test]
139 fn current_choice_marks_current_badge() {
140 let row = current_choice("Keep current (high)", Some("desc".into()), None);
141 assert_eq!(row.badge.as_deref(), Some("Current"));
142 assert_eq!(row.badge_tone, InlineTone::Current);
143 }
144
145 #[test]
146 fn choice_seeds_search_from_title_and_description() {
147 let row = choice("Flex", Some("lower cost".to_string()), None);
148 assert_eq!(row.search_value.as_deref(), Some("flex lower cost"));
149 }
150
151 #[test]
152 fn hint_is_not_a_header() {
153 let row = hint("Press Esc to reset");
154 assert_eq!(row.kind, InlineItemKind::Hint);
155 assert!(!row.is_header());
156 }
157}