1use std::path::{Path, PathBuf};
9
10pub const MAX_INLINE_CONTENT_SIZE: usize = 100 * 1024; #[derive(Debug, Clone, PartialEq, Eq)]
26pub enum PromptContentReference {
27 Inline(String),
29 FilePath {
31 path: PathBuf,
33 description: String,
35 },
36}
37
38impl PromptContentReference {
39 pub fn from_content(content: String, backup_path: &Path, description: &str) -> Self {
50 if content.len() <= MAX_INLINE_CONTENT_SIZE {
51 Self::Inline(content)
52 } else {
53 Self::FilePath {
54 path: backup_path.to_path_buf(),
55 description: description.to_string(),
56 }
57 }
58 }
59
60 pub fn inline(content: String) -> Self {
62 Self::Inline(content)
63 }
64
65 pub fn file_path(path: PathBuf, description: &str) -> Self {
67 Self::FilePath {
68 path,
69 description: description.to_string(),
70 }
71 }
72
73 pub fn is_inline(&self) -> bool {
75 matches!(self, Self::Inline(_))
76 }
77
78 pub fn render_for_template(&self) -> String {
83 match self {
84 Self::Inline(content) => content.clone(),
85 Self::FilePath { path, description } => {
86 format!(
87 "[Content too large to embed - Read from: {}]\n\
88 Description: {}\n\
89 Use your file reading tools to access this file.",
90 path.display(),
91 description
92 )
93 }
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
103pub enum DiffContentReference {
104 Inline(String),
106 UseGitDiff {
108 start_commit: String,
110 description: String,
112 },
113}
114
115impl DiffContentReference {
116 pub fn from_diff(diff_content: String, start_commit: &str) -> Self {
126 if diff_content.len() <= MAX_INLINE_CONTENT_SIZE {
127 Self::Inline(diff_content)
128 } else {
129 Self::UseGitDiff {
130 start_commit: start_commit.to_string(),
131 description: format!(
132 "Diff is {} bytes (exceeds {} limit)",
133 diff_content.len(),
134 MAX_INLINE_CONTENT_SIZE
135 ),
136 }
137 }
138 }
139
140 pub fn render_for_template(&self) -> String {
145 match self {
146 Self::Inline(content) => content.clone(),
147 Self::UseGitDiff {
148 start_commit,
149 description,
150 } => {
151 format!(
152 "[DIFF too large to embed - Use git diff instead]\n\
153 {}\n\n\
154 To see the changes, run:\n\
155 git diff {}..HEAD\n\n\
156 This shows all changes since the start of this session.",
157 description, start_commit
158 )
159 }
160 }
161 }
162
163 pub fn is_inline(&self) -> bool {
165 matches!(self, Self::Inline(_))
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum PlanContentReference {
175 Inline(String),
177 ReadFromFile {
179 primary_path: PathBuf,
181 fallback_path: Option<PathBuf>,
183 description: String,
185 },
186}
187
188impl PlanContentReference {
189 pub fn from_plan(
200 plan_content: String,
201 plan_path: &Path,
202 xml_fallback_path: Option<&Path>,
203 ) -> Self {
204 if plan_content.len() <= MAX_INLINE_CONTENT_SIZE {
205 Self::Inline(plan_content)
206 } else {
207 Self::ReadFromFile {
208 primary_path: plan_path.to_path_buf(),
209 fallback_path: xml_fallback_path.map(|p| p.to_path_buf()),
210 description: format!(
211 "Plan is {} bytes (exceeds {} limit)",
212 plan_content.len(),
213 MAX_INLINE_CONTENT_SIZE
214 ),
215 }
216 }
217 }
218
219 pub fn render_for_template(&self) -> String {
224 match self {
225 Self::Inline(content) => content.clone(),
226 Self::ReadFromFile {
227 primary_path,
228 fallback_path,
229 description,
230 } => {
231 let fallback_msg = fallback_path.as_ref().map_or(String::new(), |p| {
232 format!(
233 "\nIf {} is missing or empty, try reading: {}",
234 primary_path.display(),
235 p.display()
236 )
237 });
238 format!(
239 "[PLAN too large to embed - Read from file]\n\
240 {}\n\n\
241 Read the implementation plan from: {}{}\n\n\
242 Use your file reading tools to access the plan.",
243 description,
244 primary_path.display(),
245 fallback_msg
246 )
247 }
248 }
249 }
250
251 pub fn is_inline(&self) -> bool {
253 matches!(self, Self::Inline(_))
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
266 fn test_small_content_is_inline() {
267 let content = "Small content".to_string();
268 let reference = PromptContentReference::from_content(
269 content.clone(),
270 Path::new("/backup/path"),
271 "test",
272 );
273 assert!(reference.is_inline());
274 assert_eq!(reference.render_for_template(), content);
275 }
276
277 #[test]
278 fn test_large_content_becomes_file_path() {
279 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
280 let reference = PromptContentReference::from_content(
281 content,
282 Path::new("/backup/prompt.md"),
283 "User requirements",
284 );
285 assert!(!reference.is_inline());
286 let rendered = reference.render_for_template();
287 assert!(rendered.contains("/backup/prompt.md"));
288 assert!(rendered.contains("User requirements"));
289 }
290
291 #[test]
292 fn test_exactly_max_size_is_inline() {
293 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE);
294 let reference = PromptContentReference::from_content(
295 content.clone(),
296 Path::new("/backup/path"),
297 "test",
298 );
299 assert!(reference.is_inline());
300 }
301
302 #[test]
303 fn test_empty_content_is_inline() {
304 let reference =
305 PromptContentReference::from_content(String::new(), Path::new("/backup"), "test");
306 assert!(reference.is_inline());
307 assert_eq!(reference.render_for_template(), "");
308 }
309
310 #[test]
311 fn test_unicode_content_size_in_bytes() {
312 let emoji = "🎉".repeat(MAX_INLINE_CONTENT_SIZE / 4 + 1);
315 let reference = PromptContentReference::from_content(emoji, Path::new("/backup"), "test");
316 assert!(!reference.is_inline());
318 }
319
320 #[test]
321 fn test_prompt_inline_constructor() {
322 let content = "Direct content".to_string();
323 let reference = PromptContentReference::inline(content.clone());
324 assert!(reference.is_inline());
325 assert_eq!(reference.render_for_template(), content);
326 }
327
328 #[test]
329 fn test_prompt_file_path_constructor() {
330 let path = PathBuf::from("/path/to/file.md");
331 let reference = PromptContentReference::file_path(path.clone(), "Description");
332 assert!(!reference.is_inline());
333 let rendered = reference.render_for_template();
334 assert!(rendered.contains("/path/to/file.md"));
335 assert!(rendered.contains("Description"));
336 }
337
338 #[test]
343 fn test_small_diff_is_inline() {
344 let diff = "+added line\n-removed line".to_string();
345 let reference = DiffContentReference::from_diff(diff.clone(), "abc123");
346 assert!(reference.is_inline());
347 assert_eq!(reference.render_for_template(), diff);
348 }
349
350 #[test]
351 fn test_large_diff_uses_git_command() {
352 let diff = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
353 let reference = DiffContentReference::from_diff(diff, "abc123");
354 assert!(!reference.is_inline());
355 let rendered = reference.render_for_template();
356 assert!(rendered.contains("git diff abc123..HEAD"));
357 }
358
359 #[test]
360 fn test_diff_with_empty_start_commit() {
361 let reference =
362 DiffContentReference::from_diff("x".repeat(MAX_INLINE_CONTENT_SIZE + 1), "");
363 let rendered = reference.render_for_template();
364 assert!(rendered.contains("git diff ..HEAD"));
365 }
366
367 #[test]
368 fn test_diff_exactly_max_size_is_inline() {
369 let diff = "d".repeat(MAX_INLINE_CONTENT_SIZE);
370 let reference = DiffContentReference::from_diff(diff.clone(), "abc");
371 assert!(reference.is_inline());
372 assert_eq!(reference.render_for_template(), diff);
373 }
374
375 #[test]
380 fn test_small_plan_is_inline() {
381 let plan = "# Plan\n\n1. Do thing".to_string();
382 let reference =
383 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
384 assert!(reference.is_inline());
385 assert_eq!(reference.render_for_template(), plan);
386 }
387
388 #[test]
389 fn test_large_plan_reads_from_file() {
390 let plan = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
391 let reference = PlanContentReference::from_plan(
392 plan,
393 Path::new(".agent/PLAN.md"),
394 Some(Path::new(".agent/tmp/plan.xml")),
395 );
396 assert!(!reference.is_inline());
397 let rendered = reference.render_for_template();
398 assert!(rendered.contains(".agent/PLAN.md"));
399 assert!(rendered.contains("plan.xml"));
400 }
401
402 #[test]
403 fn test_plan_without_xml_fallback() {
404 let reference = PlanContentReference::from_plan(
405 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
406 Path::new(".agent/PLAN.md"),
407 None,
408 );
409 let rendered = reference.render_for_template();
410 assert!(rendered.contains(".agent/PLAN.md"));
411 assert!(!rendered.contains("plan.xml"));
412 }
413
414 #[test]
415 fn test_plan_exactly_max_size_is_inline() {
416 let plan = "p".repeat(MAX_INLINE_CONTENT_SIZE);
417 let reference =
418 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
419 assert!(reference.is_inline());
420 assert_eq!(reference.render_for_template(), plan);
421 }
422}