1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::event::FlowRunId;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10#[serde(tag = "kind", rename_all = "snake_case")]
11pub enum FormKind {
12 Confirm {
13 prompt: String,
14 },
15 SingleSelect {
16 prompt: String,
17 options: Vec<String>,
18 },
19 MultiSelect {
20 prompt: String,
21 options: Vec<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 min: Option<usize>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 max: Option<usize>,
26 },
27 Text {
28 prompt: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 placeholder: Option<String>,
31 #[serde(default)]
32 multiline: bool,
33 },
34}
35
36impl FormKind {
37 pub fn prompt(&self) -> &str {
38 match self {
39 Self::Confirm { prompt }
40 | Self::SingleSelect { prompt, .. }
41 | Self::MultiSelect { prompt, .. }
42 | Self::Text { prompt, .. } => prompt,
43 }
44 }
45
46 pub fn discriminator(&self) -> &'static str {
47 match self {
48 Self::Confirm { .. } => "confirm",
49 Self::SingleSelect { .. } => "single_select",
50 Self::MultiSelect { .. } => "multi_select",
51 Self::Text { .. } => "text",
52 }
53 }
54}
55
56#[derive(Debug, Clone)]
57pub struct PendingForm {
58 pub form_id: String,
59 pub run_id: FlowRunId,
60 pub tool_use_id: String,
61 pub kind: FormKind,
62 pub emitted_at: DateTime<Utc>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum FormAnswer {
70 Confirmed {
71 value: bool,
72 },
73 Selected {
74 index: usize,
75 label: String,
76 },
77 MultiSelected {
78 indices: Vec<usize>,
79 labels: Vec<String>,
80 },
81 TextEntered {
82 text: String,
83 },
84 Cancelled,
85}
86
87impl FormAnswer {
88 pub fn discriminator(&self) -> &'static str {
89 match self {
90 Self::Confirmed { .. } => "confirmed",
91 Self::Selected { .. } => "selected",
92 Self::MultiSelected { .. } => "multi_selected",
93 Self::TextEntered { .. } => "text_entered",
94 Self::Cancelled => "cancelled",
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn form_kind_serializes_with_tag() {
105 let k = FormKind::SingleSelect {
106 prompt: "pick".into(),
107 options: vec!["a".into(), "b".into()],
108 };
109 let s = serde_json::to_string(&k).unwrap();
110 assert!(s.contains(r#""kind":"single_select""#));
111 assert!(s.contains(r#""prompt":"pick""#));
112 }
113
114 #[test]
115 fn form_kind_round_trip_confirm() {
116 let k = FormKind::Confirm {
117 prompt: "sure?".into(),
118 };
119 let s = serde_json::to_string(&k).unwrap();
120 let back: FormKind = serde_json::from_str(&s).unwrap();
121 assert_eq!(back, k);
122 }
123
124 #[test]
125 fn form_kind_round_trip_multi_select_omits_empty_bounds() {
126 let k = FormKind::MultiSelect {
127 prompt: "tags".into(),
128 options: vec!["a".into()],
129 min: None,
130 max: Some(2),
131 };
132 let s = serde_json::to_string(&k).unwrap();
133 assert!(!s.contains("\"min\""));
134 assert!(s.contains("\"max\":2"));
135 let back: FormKind = serde_json::from_str(&s).unwrap();
136 assert_eq!(back, k);
137 }
138
139 #[test]
140 fn form_answer_cancelled_serializes_as_tag_only() {
141 let a = FormAnswer::Cancelled;
142 let s = serde_json::to_string(&a).unwrap();
143 assert_eq!(s, r#"{"kind":"cancelled"}"#);
144 }
145
146 #[test]
147 fn form_answer_round_trip_multi_selected() {
148 let a = FormAnswer::MultiSelected {
149 indices: vec![0, 2],
150 labels: vec!["a".into(), "c".into()],
151 };
152 let s = serde_json::to_string(&a).unwrap();
153 let back: FormAnswer = serde_json::from_str(&s).unwrap();
154 assert_eq!(back, a);
155 }
156
157 #[test]
158 fn discriminators_are_stable() {
159 assert_eq!(
160 FormKind::Text {
161 prompt: "".into(),
162 placeholder: None,
163 multiline: false,
164 }
165 .discriminator(),
166 "text"
167 );
168 assert_eq!(FormAnswer::Cancelled.discriminator(), "cancelled");
169 }
170}