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 #[must_use]
50 pub fn from_content(content: String, backup_path: &Path, description: &str) -> Self {
51 if content.len() <= MAX_INLINE_CONTENT_SIZE {
52 Self::Inline(content)
53 } else {
54 Self::FilePath {
55 path: backup_path.to_path_buf(),
56 description: description.to_string(),
57 }
58 }
59 }
60
61 #[must_use]
63 pub const fn inline(content: String) -> Self {
64 Self::Inline(content)
65 }
66
67 #[must_use]
69 pub fn file_path(path: PathBuf, description: &str) -> Self {
70 Self::FilePath {
71 path,
72 description: description.to_string(),
73 }
74 }
75
76 #[must_use]
78 pub const fn is_inline(&self) -> bool {
79 matches!(self, Self::Inline(_))
80 }
81
82 #[must_use]
87 pub fn render_for_template(&self) -> String {
88 match self {
89 Self::Inline(content) => content.clone(),
90 Self::FilePath { path, description } => {
91 format!(
92 "[Content too large to embed - Read from: {}]\n\
93 Description: {}\n\
94 Use your file reading tools to access this file.",
95 path.display(),
96 description
97 )
98 }
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum DiffContentReference {
110 Inline(String),
112 ReadFromFile {
114 path: PathBuf,
116 start_commit: String,
118 description: String,
120 },
121}
122
123impl DiffContentReference {
124 #[must_use]
134 pub fn from_diff(diff_content: String, start_commit: &str, diff_path: &Path) -> Self {
135 if diff_content.len() <= MAX_INLINE_CONTENT_SIZE {
136 Self::Inline(diff_content)
137 } else {
138 Self::ReadFromFile {
139 path: diff_path.to_path_buf(),
140 start_commit: start_commit.to_string(),
141 description: format!(
142 "Diff is {} bytes (exceeds {} limit)",
143 diff_content.len(),
144 MAX_INLINE_CONTENT_SIZE
145 ),
146 }
147 }
148 }
149
150 #[must_use]
156 pub fn render_for_template(&self) -> String {
157 match self {
158 Self::Inline(content) => content.clone(),
159 Self::ReadFromFile {
160 path,
161 start_commit,
162 description,
163 } => {
164 if start_commit.is_empty() {
165 format!(
166 "[DIFF too large to embed - Read from file]\n\
167 {}\n\n\
168 Read the diff from: {}\n\
169 If this file is missing or unavailable, regenerate it with git (last resort):\n\
170 - Unstaged changes: git diff\n\
171 - Staged changes: git diff --cached\n\
172 - Untracked files: git ls-files --others --exclude-standard\n",
173 description,
174 path.display(),
175 )
176 } else {
177 format!(
178 "[DIFF too large to embed - Read from file]\n\
179 {}\n\n\
180 Read the diff from: {}\n\
181 If this file is missing or unavailable, regenerate it with git (last resort):\n\
182 - Unstaged changes: git diff {}\n\
183 - Staged changes: git diff --cached {}\n\
184 - Untracked files: git ls-files --others --exclude-standard\n",
185 description,
186 path.display(),
187 start_commit,
188 start_commit,
189 )
190 }
191 }
192 }
193 }
194
195 #[must_use]
197 pub const fn is_inline(&self) -> bool {
198 matches!(self, Self::Inline(_))
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
207pub enum PlanContentReference {
208 Inline(String),
210 ReadFromFile {
212 primary_path: PathBuf,
214 fallback_path: Option<PathBuf>,
216 description: String,
218 },
219}
220
221impl PlanContentReference {
222 #[must_use]
233 pub fn from_plan(
234 plan_content: String,
235 plan_path: &Path,
236 xml_fallback_path: Option<&Path>,
237 ) -> Self {
238 if plan_content.len() <= MAX_INLINE_CONTENT_SIZE {
239 Self::Inline(plan_content)
240 } else {
241 Self::ReadFromFile {
242 primary_path: plan_path.to_path_buf(),
243 fallback_path: xml_fallback_path.map(std::path::Path::to_path_buf),
244 description: format!(
245 "Plan is {} bytes (exceeds {} limit)",
246 plan_content.len(),
247 MAX_INLINE_CONTENT_SIZE
248 ),
249 }
250 }
251 }
252
253 #[must_use]
258 pub fn render_for_template(&self) -> String {
259 match self {
260 Self::Inline(content) => content.clone(),
261 Self::ReadFromFile {
262 primary_path,
263 fallback_path,
264 description,
265 } => {
266 let fallback_msg = fallback_path.as_ref().map_or(String::new(), |p| {
267 format!(
268 "\nIf {} is missing or empty, try reading: {}",
269 primary_path.display(),
270 p.display()
271 )
272 });
273 format!(
274 "[PLAN too large to embed - Read from file]\n\
275 {}\n\n\
276 Read the implementation plan from: {}{}\n\n\
277 Use your file reading tools to access the plan.",
278 description,
279 primary_path.display(),
280 fallback_msg
281 )
282 }
283 }
284 }
285
286 #[must_use]
288 pub const fn is_inline(&self) -> bool {
289 matches!(self, Self::Inline(_))
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
302 fn test_small_content_is_inline() {
303 let content = "Small content".to_string();
304 let reference = PromptContentReference::from_content(
305 content.clone(),
306 Path::new("/backup/path"),
307 "test",
308 );
309 assert!(reference.is_inline());
310 assert_eq!(reference.render_for_template(), content);
311 }
312
313 #[test]
314 fn test_large_content_becomes_file_path() {
315 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
316 let reference = PromptContentReference::from_content(
317 content,
318 Path::new("/backup/prompt.md"),
319 "User requirements",
320 );
321 assert!(!reference.is_inline());
322 let rendered = reference.render_for_template();
323 assert!(rendered.contains("/backup/prompt.md"));
324 assert!(rendered.contains("User requirements"));
325 }
326
327 #[test]
328 fn test_exactly_max_size_is_inline() {
329 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE);
330 let reference =
331 PromptContentReference::from_content(content, Path::new("/backup/path"), "test");
332 assert!(reference.is_inline());
333 }
334
335 #[test]
336 fn test_empty_content_is_inline() {
337 let reference =
338 PromptContentReference::from_content(String::new(), Path::new("/backup"), "test");
339 assert!(reference.is_inline());
340 assert_eq!(reference.render_for_template(), "");
341 }
342
343 #[test]
344 fn test_unicode_content_size_in_bytes() {
345 let emoji = "🎉".repeat(MAX_INLINE_CONTENT_SIZE / 4 + 1);
348 let reference = PromptContentReference::from_content(emoji, Path::new("/backup"), "test");
349 assert!(!reference.is_inline());
351 }
352
353 #[test]
354 fn test_prompt_inline_constructor() {
355 let content = "Direct content".to_string();
356 let reference = PromptContentReference::inline(content.clone());
357 assert!(reference.is_inline());
358 assert_eq!(reference.render_for_template(), content);
359 }
360
361 #[test]
362 fn test_prompt_file_path_constructor() {
363 let path = PathBuf::from("/path/to/file.md");
364 let reference = PromptContentReference::file_path(path, "Description");
365 assert!(!reference.is_inline());
366 let rendered = reference.render_for_template();
367 assert!(rendered.contains("/path/to/file.md"));
368 assert!(rendered.contains("Description"));
369 }
370
371 #[test]
376 fn test_small_diff_is_inline() {
377 let diff = "+added line\n-removed line".to_string();
378 let reference =
379 DiffContentReference::from_diff(diff.clone(), "abc123", Path::new("/backup/diff.txt"));
380 assert!(reference.is_inline());
381 assert_eq!(reference.render_for_template(), diff);
382 }
383
384 #[test]
385 fn test_large_diff_reads_from_file() {
386 let diff = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
387 let reference =
388 DiffContentReference::from_diff(diff, "abc123", Path::new("/backup/diff.txt"));
389 assert!(!reference.is_inline());
390 let rendered = reference.render_for_template();
391 assert!(rendered.contains("/backup/diff.txt"));
392 assert!(rendered.contains("git diff"));
393 }
394
395 #[test]
396 fn test_diff_with_empty_start_commit_includes_git_fallback() {
397 let reference = DiffContentReference::from_diff(
398 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
399 "",
400 Path::new("/backup/diff.txt"),
401 );
402 let rendered = reference.render_for_template();
403 assert!(rendered.contains("/backup/diff.txt"));
404 assert!(rendered.contains("Unstaged changes: git diff"));
405 assert!(rendered.contains("Staged changes: git diff --cached"));
406 }
407
408 #[test]
409 fn test_diff_exactly_max_size_is_inline() {
410 let diff = "d".repeat(MAX_INLINE_CONTENT_SIZE);
411 let reference =
412 DiffContentReference::from_diff(diff.clone(), "abc", Path::new("/backup/diff.txt"));
413 assert!(reference.is_inline());
414 assert_eq!(reference.render_for_template(), diff);
415 }
416
417 #[test]
422 fn test_small_plan_is_inline() {
423 let plan = "# Plan\n\n1. Do thing".to_string();
424 let reference =
425 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
426 assert!(reference.is_inline());
427 assert_eq!(reference.render_for_template(), plan);
428 }
429
430 #[test]
431 fn test_large_plan_reads_from_file() {
432 let plan = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
433 let reference = PlanContentReference::from_plan(
434 plan,
435 Path::new(".agent/PLAN.md"),
436 Some(Path::new(".agent/tmp/plan.xml")),
437 );
438 assert!(!reference.is_inline());
439 let rendered = reference.render_for_template();
440 assert!(rendered.contains(".agent/PLAN.md"));
441 assert!(rendered.contains("plan.xml"));
442 }
443
444 #[test]
445 fn test_plan_without_xml_fallback() {
446 let reference = PlanContentReference::from_plan(
447 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
448 Path::new(".agent/PLAN.md"),
449 None,
450 );
451 let rendered = reference.render_for_template();
452 assert!(rendered.contains(".agent/PLAN.md"));
453 assert!(!rendered.contains("plan.xml"));
454 }
455
456 #[test]
457 fn test_plan_exactly_max_size_is_inline() {
458 let plan = "p".repeat(MAX_INLINE_CONTENT_SIZE);
459 let reference =
460 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
461 assert!(reference.is_inline());
462 assert_eq!(reference.render_for_template(), plan);
463 }
464}