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 ReadFromFile {
108 path: PathBuf,
110 start_commit: String,
112 description: String,
114 },
115}
116
117impl DiffContentReference {
118 pub fn from_diff(diff_content: String, start_commit: &str, diff_path: &Path) -> Self {
128 if diff_content.len() <= MAX_INLINE_CONTENT_SIZE {
129 Self::Inline(diff_content)
130 } else {
131 Self::ReadFromFile {
132 path: diff_path.to_path_buf(),
133 start_commit: start_commit.to_string(),
134 description: format!(
135 "Diff is {} bytes (exceeds {} limit)",
136 diff_content.len(),
137 MAX_INLINE_CONTENT_SIZE
138 ),
139 }
140 }
141 }
142
143 pub fn render_for_template(&self) -> String {
148 match self {
149 Self::Inline(content) => content.clone(),
150 Self::ReadFromFile {
151 path,
152 start_commit,
153 description,
154 } => {
155 if start_commit.is_empty() {
163 format!(
164 "[DIFF too large to embed - Read from file]\n\
165 {}\n\n\
166 Read the diff from: {}\n\
167 If this file is missing or unavailable, regenerate it with git (last resort):\n\
168 - Unstaged changes: git diff\n\
169 - Staged changes: git diff --cached\n\
170 - Untracked files: git ls-files --others --exclude-standard\n",
171 description,
172 path.display(),
173 )
174 } else {
175 format!(
176 "[DIFF too large to embed - Read from file]\n\
177 {}\n\n\
178 Read the diff from: {}\n\
179 If this file is missing or unavailable, regenerate it with git (last resort):\n\
180 - Unstaged changes: git diff {}\n\
181 - Staged changes: git diff --cached {}\n\
182 - Untracked files: git ls-files --others --exclude-standard\n",
183 description,
184 path.display(),
185 start_commit,
186 start_commit,
187 )
188 }
189 }
190 }
191 }
192
193 pub fn is_inline(&self) -> bool {
195 matches!(self, Self::Inline(_))
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum PlanContentReference {
205 Inline(String),
207 ReadFromFile {
209 primary_path: PathBuf,
211 fallback_path: Option<PathBuf>,
213 description: String,
215 },
216}
217
218impl PlanContentReference {
219 pub fn from_plan(
230 plan_content: String,
231 plan_path: &Path,
232 xml_fallback_path: Option<&Path>,
233 ) -> Self {
234 if plan_content.len() <= MAX_INLINE_CONTENT_SIZE {
235 Self::Inline(plan_content)
236 } else {
237 Self::ReadFromFile {
238 primary_path: plan_path.to_path_buf(),
239 fallback_path: xml_fallback_path.map(|p| p.to_path_buf()),
240 description: format!(
241 "Plan is {} bytes (exceeds {} limit)",
242 plan_content.len(),
243 MAX_INLINE_CONTENT_SIZE
244 ),
245 }
246 }
247 }
248
249 pub fn render_for_template(&self) -> String {
254 match self {
255 Self::Inline(content) => content.clone(),
256 Self::ReadFromFile {
257 primary_path,
258 fallback_path,
259 description,
260 } => {
261 let fallback_msg = fallback_path.as_ref().map_or(String::new(), |p| {
262 format!(
263 "\nIf {} is missing or empty, try reading: {}",
264 primary_path.display(),
265 p.display()
266 )
267 });
268 format!(
269 "[PLAN too large to embed - Read from file]\n\
270 {}\n\n\
271 Read the implementation plan from: {}{}\n\n\
272 Use your file reading tools to access the plan.",
273 description,
274 primary_path.display(),
275 fallback_msg
276 )
277 }
278 }
279 }
280
281 pub fn is_inline(&self) -> bool {
283 matches!(self, Self::Inline(_))
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
296 fn test_small_content_is_inline() {
297 let content = "Small content".to_string();
298 let reference = PromptContentReference::from_content(
299 content.clone(),
300 Path::new("/backup/path"),
301 "test",
302 );
303 assert!(reference.is_inline());
304 assert_eq!(reference.render_for_template(), content);
305 }
306
307 #[test]
308 fn test_large_content_becomes_file_path() {
309 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
310 let reference = PromptContentReference::from_content(
311 content,
312 Path::new("/backup/prompt.md"),
313 "User requirements",
314 );
315 assert!(!reference.is_inline());
316 let rendered = reference.render_for_template();
317 assert!(rendered.contains("/backup/prompt.md"));
318 assert!(rendered.contains("User requirements"));
319 }
320
321 #[test]
322 fn test_exactly_max_size_is_inline() {
323 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE);
324 let reference = PromptContentReference::from_content(
325 content.clone(),
326 Path::new("/backup/path"),
327 "test",
328 );
329 assert!(reference.is_inline());
330 }
331
332 #[test]
333 fn test_empty_content_is_inline() {
334 let reference =
335 PromptContentReference::from_content(String::new(), Path::new("/backup"), "test");
336 assert!(reference.is_inline());
337 assert_eq!(reference.render_for_template(), "");
338 }
339
340 #[test]
341 fn test_unicode_content_size_in_bytes() {
342 let emoji = "🎉".repeat(MAX_INLINE_CONTENT_SIZE / 4 + 1);
345 let reference = PromptContentReference::from_content(emoji, Path::new("/backup"), "test");
346 assert!(!reference.is_inline());
348 }
349
350 #[test]
351 fn test_prompt_inline_constructor() {
352 let content = "Direct content".to_string();
353 let reference = PromptContentReference::inline(content.clone());
354 assert!(reference.is_inline());
355 assert_eq!(reference.render_for_template(), content);
356 }
357
358 #[test]
359 fn test_prompt_file_path_constructor() {
360 let path = PathBuf::from("/path/to/file.md");
361 let reference = PromptContentReference::file_path(path.clone(), "Description");
362 assert!(!reference.is_inline());
363 let rendered = reference.render_for_template();
364 assert!(rendered.contains("/path/to/file.md"));
365 assert!(rendered.contains("Description"));
366 }
367
368 #[test]
373 fn test_small_diff_is_inline() {
374 let diff = "+added line\n-removed line".to_string();
375 let reference =
376 DiffContentReference::from_diff(diff.clone(), "abc123", Path::new("/backup/diff.txt"));
377 assert!(reference.is_inline());
378 assert_eq!(reference.render_for_template(), diff);
379 }
380
381 #[test]
382 fn test_large_diff_reads_from_file_with_git_fallback() {
383 let diff = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
384 let reference =
385 DiffContentReference::from_diff(diff, "abc123", Path::new("/backup/diff.txt"));
386 assert!(!reference.is_inline());
387 let rendered = reference.render_for_template();
388 assert!(rendered.contains("/backup/diff.txt"));
389 assert!(rendered.contains("git diff abc123"));
390 assert!(rendered.contains("git diff --cached abc123"));
391 }
392
393 #[test]
394 fn test_diff_with_empty_start_commit() {
395 let reference = DiffContentReference::from_diff(
396 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
397 "",
398 Path::new("/backup/diff.txt"),
399 );
400 let rendered = reference.render_for_template();
401 assert!(rendered.contains("Unstaged changes: git diff"));
402 assert!(rendered.contains("Staged changes: git diff --cached"));
403 }
404
405 #[test]
406 fn test_diff_exactly_max_size_is_inline() {
407 let diff = "d".repeat(MAX_INLINE_CONTENT_SIZE);
408 let reference =
409 DiffContentReference::from_diff(diff.clone(), "abc", Path::new("/backup/diff.txt"));
410 assert!(reference.is_inline());
411 assert_eq!(reference.render_for_template(), diff);
412 }
413
414 #[test]
419 fn test_small_plan_is_inline() {
420 let plan = "# Plan\n\n1. Do thing".to_string();
421 let reference =
422 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
423 assert!(reference.is_inline());
424 assert_eq!(reference.render_for_template(), plan);
425 }
426
427 #[test]
428 fn test_large_plan_reads_from_file() {
429 let plan = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
430 let reference = PlanContentReference::from_plan(
431 plan,
432 Path::new(".agent/PLAN.md"),
433 Some(Path::new(".agent/tmp/plan.xml")),
434 );
435 assert!(!reference.is_inline());
436 let rendered = reference.render_for_template();
437 assert!(rendered.contains(".agent/PLAN.md"));
438 assert!(rendered.contains("plan.xml"));
439 }
440
441 #[test]
442 fn test_plan_without_xml_fallback() {
443 let reference = PlanContentReference::from_plan(
444 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
445 Path::new(".agent/PLAN.md"),
446 None,
447 );
448 let rendered = reference.render_for_template();
449 assert!(rendered.contains(".agent/PLAN.md"));
450 assert!(!rendered.contains("plan.xml"));
451 }
452
453 #[test]
454 fn test_plan_exactly_max_size_is_inline() {
455 let plan = "p".repeat(MAX_INLINE_CONTENT_SIZE);
456 let reference =
457 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
458 assert!(reference.is_inline());
459 assert_eq!(reference.render_for_template(), plan);
460 }
461}