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 format!(
156 "[DIFF too large to embed - Read from file]\n\
157 {}\n\n\
158 Read the diff from: {}\n\
159 If this file is missing or unavailable, run:\n\
160 git diff {}..HEAD\n\n\
161 This shows all changes since the start of this session.",
162 description,
163 path.display(),
164 start_commit
165 )
166 }
167 }
168 }
169
170 pub fn is_inline(&self) -> bool {
172 matches!(self, Self::Inline(_))
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum PlanContentReference {
182 Inline(String),
184 ReadFromFile {
186 primary_path: PathBuf,
188 fallback_path: Option<PathBuf>,
190 description: String,
192 },
193}
194
195impl PlanContentReference {
196 pub fn from_plan(
207 plan_content: String,
208 plan_path: &Path,
209 xml_fallback_path: Option<&Path>,
210 ) -> Self {
211 if plan_content.len() <= MAX_INLINE_CONTENT_SIZE {
212 Self::Inline(plan_content)
213 } else {
214 Self::ReadFromFile {
215 primary_path: plan_path.to_path_buf(),
216 fallback_path: xml_fallback_path.map(|p| p.to_path_buf()),
217 description: format!(
218 "Plan is {} bytes (exceeds {} limit)",
219 plan_content.len(),
220 MAX_INLINE_CONTENT_SIZE
221 ),
222 }
223 }
224 }
225
226 pub fn render_for_template(&self) -> String {
231 match self {
232 Self::Inline(content) => content.clone(),
233 Self::ReadFromFile {
234 primary_path,
235 fallback_path,
236 description,
237 } => {
238 let fallback_msg = fallback_path.as_ref().map_or(String::new(), |p| {
239 format!(
240 "\nIf {} is missing or empty, try reading: {}",
241 primary_path.display(),
242 p.display()
243 )
244 });
245 format!(
246 "[PLAN too large to embed - Read from file]\n\
247 {}\n\n\
248 Read the implementation plan from: {}{}\n\n\
249 Use your file reading tools to access the plan.",
250 description,
251 primary_path.display(),
252 fallback_msg
253 )
254 }
255 }
256 }
257
258 pub fn is_inline(&self) -> bool {
260 matches!(self, Self::Inline(_))
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
273 fn test_small_content_is_inline() {
274 let content = "Small content".to_string();
275 let reference = PromptContentReference::from_content(
276 content.clone(),
277 Path::new("/backup/path"),
278 "test",
279 );
280 assert!(reference.is_inline());
281 assert_eq!(reference.render_for_template(), content);
282 }
283
284 #[test]
285 fn test_large_content_becomes_file_path() {
286 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
287 let reference = PromptContentReference::from_content(
288 content,
289 Path::new("/backup/prompt.md"),
290 "User requirements",
291 );
292 assert!(!reference.is_inline());
293 let rendered = reference.render_for_template();
294 assert!(rendered.contains("/backup/prompt.md"));
295 assert!(rendered.contains("User requirements"));
296 }
297
298 #[test]
299 fn test_exactly_max_size_is_inline() {
300 let content = "x".repeat(MAX_INLINE_CONTENT_SIZE);
301 let reference = PromptContentReference::from_content(
302 content.clone(),
303 Path::new("/backup/path"),
304 "test",
305 );
306 assert!(reference.is_inline());
307 }
308
309 #[test]
310 fn test_empty_content_is_inline() {
311 let reference =
312 PromptContentReference::from_content(String::new(), Path::new("/backup"), "test");
313 assert!(reference.is_inline());
314 assert_eq!(reference.render_for_template(), "");
315 }
316
317 #[test]
318 fn test_unicode_content_size_in_bytes() {
319 let emoji = "🎉".repeat(MAX_INLINE_CONTENT_SIZE / 4 + 1);
322 let reference = PromptContentReference::from_content(emoji, Path::new("/backup"), "test");
323 assert!(!reference.is_inline());
325 }
326
327 #[test]
328 fn test_prompt_inline_constructor() {
329 let content = "Direct content".to_string();
330 let reference = PromptContentReference::inline(content.clone());
331 assert!(reference.is_inline());
332 assert_eq!(reference.render_for_template(), content);
333 }
334
335 #[test]
336 fn test_prompt_file_path_constructor() {
337 let path = PathBuf::from("/path/to/file.md");
338 let reference = PromptContentReference::file_path(path.clone(), "Description");
339 assert!(!reference.is_inline());
340 let rendered = reference.render_for_template();
341 assert!(rendered.contains("/path/to/file.md"));
342 assert!(rendered.contains("Description"));
343 }
344
345 #[test]
350 fn test_small_diff_is_inline() {
351 let diff = "+added line\n-removed line".to_string();
352 let reference =
353 DiffContentReference::from_diff(diff.clone(), "abc123", Path::new("/backup/diff.txt"));
354 assert!(reference.is_inline());
355 assert_eq!(reference.render_for_template(), diff);
356 }
357
358 #[test]
359 fn test_large_diff_reads_from_file_with_git_fallback() {
360 let diff = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
361 let reference =
362 DiffContentReference::from_diff(diff, "abc123", Path::new("/backup/diff.txt"));
363 assert!(!reference.is_inline());
364 let rendered = reference.render_for_template();
365 assert!(rendered.contains("/backup/diff.txt"));
366 assert!(rendered.contains("git diff abc123..HEAD"));
367 }
368
369 #[test]
370 fn test_diff_with_empty_start_commit() {
371 let reference = DiffContentReference::from_diff(
372 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
373 "",
374 Path::new("/backup/diff.txt"),
375 );
376 let rendered = reference.render_for_template();
377 assert!(rendered.contains("git diff ..HEAD"));
378 }
379
380 #[test]
381 fn test_diff_exactly_max_size_is_inline() {
382 let diff = "d".repeat(MAX_INLINE_CONTENT_SIZE);
383 let reference =
384 DiffContentReference::from_diff(diff.clone(), "abc", Path::new("/backup/diff.txt"));
385 assert!(reference.is_inline());
386 assert_eq!(reference.render_for_template(), diff);
387 }
388
389 #[test]
394 fn test_small_plan_is_inline() {
395 let plan = "# Plan\n\n1. Do thing".to_string();
396 let reference =
397 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
398 assert!(reference.is_inline());
399 assert_eq!(reference.render_for_template(), plan);
400 }
401
402 #[test]
403 fn test_large_plan_reads_from_file() {
404 let plan = "x".repeat(MAX_INLINE_CONTENT_SIZE + 1);
405 let reference = PlanContentReference::from_plan(
406 plan,
407 Path::new(".agent/PLAN.md"),
408 Some(Path::new(".agent/tmp/plan.xml")),
409 );
410 assert!(!reference.is_inline());
411 let rendered = reference.render_for_template();
412 assert!(rendered.contains(".agent/PLAN.md"));
413 assert!(rendered.contains("plan.xml"));
414 }
415
416 #[test]
417 fn test_plan_without_xml_fallback() {
418 let reference = PlanContentReference::from_plan(
419 "x".repeat(MAX_INLINE_CONTENT_SIZE + 1),
420 Path::new(".agent/PLAN.md"),
421 None,
422 );
423 let rendered = reference.render_for_template();
424 assert!(rendered.contains(".agent/PLAN.md"));
425 assert!(!rendered.contains("plan.xml"));
426 }
427
428 #[test]
429 fn test_plan_exactly_max_size_is_inline() {
430 let plan = "p".repeat(MAX_INLINE_CONTENT_SIZE);
431 let reference =
432 PlanContentReference::from_plan(plan.clone(), Path::new(".agent/PLAN.md"), None);
433 assert!(reference.is_inline());
434 assert_eq!(reference.render_for_template(), plan);
435 }
436}