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