1use agentic_tools_core::fmt::{TextFormat, TextOptions};
7
8use crate::documents::{ActiveDocuments, WriteDocumentOk};
9use crate::mcp::{AddReferenceOk, ReferencesList, TemplateResponse};
10use crate::utils::human_size;
11
12impl TextFormat for WriteDocumentOk {
13 fn fmt_text(&self, _opts: &TextOptions) -> String {
14 format!(
15 "\u{2713} Created {}\n Size: {}",
16 self.path,
17 human_size(self.bytes_written)
18 )
19 }
20}
21
22impl TextFormat for ActiveDocuments {
23 fn fmt_text(&self, _opts: &TextOptions) -> String {
24 if self.files.is_empty() {
25 return format!(
26 "Active base: {}\nFiles (relative to base):\n<none>",
27 self.base
28 );
29 }
30 let mut out = format!("Active base: {}\nFiles (relative to base):", self.base);
31 for f in &self.files {
32 let rel = f
33 .path
34 .strip_prefix(&format!("{}/", self.base.trim_end_matches('/')))
35 .unwrap_or(&f.path);
36 let ts = match chrono::DateTime::parse_from_rfc3339(&f.modified) {
37 Ok(dt) => dt
38 .with_timezone(&chrono::Utc)
39 .format("%Y-%m-%d %H:%M UTC")
40 .to_string(),
41 Err(_) => f.modified.clone(),
42 };
43 out.push_str(&format!("\n{} @ {}", rel, ts));
44 }
45 out
46 }
47}
48
49impl TextFormat for ReferencesList {
50 fn fmt_text(&self, _opts: &TextOptions) -> String {
51 if self.entries.is_empty() {
52 return format!("References base: {}\n<none>", self.base);
53 }
54 let mut out = format!("References base: {}", self.base);
55 for e in &self.entries {
56 let rel = e
57 .path
58 .strip_prefix(&format!("{}/", self.base.trim_end_matches('/')))
59 .unwrap_or(&e.path);
60 match &e.description {
61 Some(desc) if !desc.trim().is_empty() => {
62 out.push_str(&format!("\n{} \u{2014} {}", rel, desc));
63 }
64 _ => {
65 out.push_str(&format!("\n{}", rel));
66 }
67 }
68 }
69 out
70 }
71}
72
73impl TextFormat for AddReferenceOk {
74 fn fmt_text(&self, _opts: &TextOptions) -> String {
75 let mut out = String::new();
76 if self.already_existed {
77 out.push_str("\u{2713} Reference already exists (idempotent)\n");
78 } else {
79 out.push_str("\u{2713} Added reference\n");
80 }
81 out.push_str(&format!(
82 " URL: {}\n Org/Repo: {}/{}\n Mount: {}\n Target: {}",
83 self.url, self.org, self.repo, self.mount_path, self.mount_target
84 ));
85 if let Some(mp) = &self.mapping_path {
86 out.push_str(&format!("\n Mapping: {}", mp));
87 } else {
88 out.push_str("\n Mapping: <none>");
89 }
90 out.push_str(&format!(
91 "\n Config updated: {}\n Cloned: {}\n Mounted: {}",
92 self.config_updated, self.cloned, self.mounted
93 ));
94 if !self.warnings.is_empty() {
95 out.push_str("\nWarnings:");
96 for w in &self.warnings {
97 out.push_str(&format!("\n- {}", w));
98 }
99 }
100 out
101 }
102}
103
104impl TextFormat for TemplateResponse {
105 fn fmt_text(&self, _opts: &TextOptions) -> String {
106 let ty = self.template_type.label();
107 let content = self.template_type.content();
108 let guidance = self.template_type.guidance();
109 format!(
110 "Here is the {} template:\n\n```markdown\n{}\n```\n\n{}",
111 ty, content, guidance
112 )
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::documents::DocumentInfo;
120 use crate::mcp::{ReferenceItem, TemplateType};
121
122 #[test]
123 fn write_document_text_format() {
124 let v = WriteDocumentOk {
125 path: "./thoughts/x/research/test.md".into(),
126 bytes_written: 2048,
127 };
128 let tf = v.fmt_text(&TextOptions::default());
129 assert!(tf.contains("\u{2713} Created"));
130 assert!(tf.contains("2.0 KB"));
131 }
132
133 #[test]
134 fn active_documents_empty_text_format() {
135 let docs = ActiveDocuments {
136 base: "./thoughts/branch".into(),
137 files: vec![],
138 };
139 let tf = docs.fmt_text(&TextOptions::default());
140 assert!(tf.contains("<none>"));
141 }
142
143 #[test]
144 fn active_documents_with_files_text_format() {
145 let docs = ActiveDocuments {
146 base: "./thoughts/feature".into(),
147 files: vec![DocumentInfo {
148 path: "./thoughts/feature/research/test.md".into(),
149 doc_type: "research".into(),
150 size: 1024,
151 modified: "2025-10-15T12:00:00Z".into(),
152 }],
153 };
154 let tf = docs.fmt_text(&TextOptions::default());
155 assert!(tf.contains("research/test.md"));
156 }
157
158 #[test]
159 fn references_list_empty_text_format() {
160 let refs = ReferencesList {
161 base: "references".into(),
162 entries: vec![],
163 };
164 let tf = refs.fmt_text(&TextOptions::default());
165 assert!(tf.contains("<none>"));
166 }
167
168 #[test]
169 fn references_list_with_descriptions_text_format() {
170 let refs = ReferencesList {
171 base: "references".into(),
172 entries: vec![
173 ReferenceItem {
174 path: "references/org/repo1".into(),
175 description: Some("First repo".into()),
176 },
177 ReferenceItem {
178 path: "references/org/repo2".into(),
179 description: None,
180 },
181 ],
182 };
183 let tf = refs.fmt_text(&TextOptions::default());
184 assert!(tf.contains("org/repo1 \u{2014} First repo"));
185 assert!(tf.contains("org/repo2"));
186 }
187
188 #[test]
189 fn add_reference_ok_text_format() {
190 let ok = AddReferenceOk {
191 url: "https://github.com/org/repo".into(),
192 org: "org".into(),
193 repo: "repo".into(),
194 mount_path: "references/org/repo".into(),
195 mount_target: "/abs/.thoughts-data/references/org/repo".into(),
196 mapping_path: Some("/home/user/.thoughts/clones/repo".into()),
197 already_existed: false,
198 config_updated: true,
199 cloned: true,
200 mounted: true,
201 warnings: vec!["note".into()],
202 };
203 let tf = ok.fmt_text(&TextOptions::default());
204 assert!(tf.contains("\u{2713} Added reference"));
205 assert!(tf.contains("Warnings:\n- note"));
206 }
207
208 #[test]
209 fn add_reference_ok_already_existed_text_format() {
210 let ok = AddReferenceOk {
211 url: "https://github.com/org/repo".into(),
212 org: "org".into(),
213 repo: "repo".into(),
214 mount_path: "references/org/repo".into(),
215 mount_target: "/abs/.thoughts-data/references/org/repo".into(),
216 mapping_path: None,
217 already_existed: true,
218 config_updated: false,
219 cloned: false,
220 mounted: true,
221 warnings: vec![],
222 };
223 let tf = ok.fmt_text(&TextOptions::default());
224 assert!(tf.contains("\u{2713} Reference already exists (idempotent)"));
225 assert!(tf.contains("Mapping: <none>"));
226 }
227
228 #[test]
229 fn template_response_text_format() {
230 let resp = TemplateResponse {
231 template_type: TemplateType::Research,
232 };
233 let tf = resp.fmt_text(&TextOptions::default());
234 assert!(tf.starts_with("Here is the research template:"));
235 assert!(tf.contains("```markdown"));
236 }
237}