vtcode_commons/ui_protocol/
input.rs1use std::ops::Deref;
8
9#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ContentPart {
16 Text { text: String },
17 Image { data: Box<str>, media_type: String },
18}
19
20impl ContentPart {
21 pub fn text(text: impl Into<String>) -> Self {
22 Self::Text { text: text.into() }
23 }
24
25 pub fn image(data: impl Into<String>, media_type: impl Into<String>) -> Self {
26 Self::Image {
27 data: data.into().into_boxed_str(),
28 media_type: media_type.into(),
29 }
30 }
31
32 pub fn is_image(&self) -> bool {
33 matches!(self, Self::Image { .. })
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct SubmittedInput {
39 pub text: String,
40 pub attachments: Vec<ContentPart>,
41 pub batchable: bool,
47}
48
49impl SubmittedInput {
50 pub fn new(text: impl Into<String>, attachments: Vec<ContentPart>) -> Self {
51 Self { text: text.into(), attachments, batchable: false }
52 }
53
54 fn text_only(text: impl Into<String>) -> Self {
55 Self::new(text, Vec::new())
56 }
57
58 pub fn batchable(mut self) -> Self {
60 self.batchable = true;
61 self
62 }
63
64 pub fn trim_text(self) -> Self {
65 Self {
66 text: self.text.trim().to_string(),
67 attachments: self.attachments,
68 batchable: self.batchable,
69 }
70 }
71
72 pub fn is_empty(&self) -> bool {
73 self.text.trim().is_empty() && self.attachments.is_empty()
74 }
75
76 pub fn has_attachments(&self) -> bool {
77 !self.attachments.is_empty()
78 }
79
80 pub fn is_steerable_text(&self) -> bool {
84 !self.has_attachments() && !self.text.trim_start().starts_with('/')
85 }
86}
87
88impl From<String> for SubmittedInput {
89 fn from(text: String) -> Self {
90 Self::text_only(text)
91 }
92}
93
94impl From<&str> for SubmittedInput {
95 fn from(text: &str) -> Self {
96 Self::text_only(text)
97 }
98}
99
100impl Deref for SubmittedInput {
101 type Target = str;
102
103 fn deref(&self) -> &Self::Target {
104 &self.text
105 }
106}
107
108impl PartialEq<&str> for SubmittedInput {
109 fn eq(&self, other: &&str) -> bool {
110 self.text == *other
111 }
112}
113
114impl PartialEq<&str> for &SubmittedInput {
115 fn eq(&self, other: &&str) -> bool {
116 self.text == *other
117 }
118}
119
120impl PartialEq<String> for SubmittedInput {
121 fn eq(&self, other: &String) -> bool {
122 self.text == *other
123 }
124}
125
126impl PartialEq<&String> for SubmittedInput {
127 fn eq(&self, other: &&String) -> bool {
128 self.text == **other
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn steerable_text_matrix() {
138 assert!(SubmittedInput::from("run tests").is_steerable_text());
139 assert!(!SubmittedInput::from("/compact now").is_steerable_text());
140 assert!(!SubmittedInput::from(" /compact now").is_steerable_text());
141 assert!(
142 !SubmittedInput::new("describe this", vec![ContentPart::image("aGVsbG8=", "image/png")])
143 .is_steerable_text()
144 );
145 }
146
147 #[test]
148 fn content_part_constructors_keep_shape() {
149 let text = ContentPart::text("hello");
150 assert!(!text.is_image());
151 assert!(matches!(text, ContentPart::Text { .. }));
152
153 let image = ContentPart::image("aGVsbG8=", "image/png");
154 assert!(image.is_image());
155 assert!(matches!(
156 image,
157 ContentPart::Image { ref media_type, .. } if media_type == "image/png"
158 ));
159 }
160
161 #[test]
162 fn batchable_and_trim_roundtrip() {
163 let input = SubmittedInput::from(" padded ").batchable().trim_text();
164 assert!(input.batchable);
165 assert_eq!(input.text, "padded");
166 assert_eq!(&*input, "padded");
167 }
168}