From ab352f669e0336aba01631c9784b848b34a04102 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 12 Nov 2025 11:55:25 -0500 Subject: [PATCH] Gracefully handle `@mention`-ing large files with no outlines (#42543) Closes #32098 Release Notes: - In the Agent panel, when `@mention`-ing large files with no outline, their first 1KB is now added to context --- crates/agent/src/outline.rs | 78 +++++++++++++++++++++++ crates/agent_ui/src/acp/message_editor.rs | 36 +++++++---- crates/agent_ui/src/context.rs | 14 ++-- 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/crates/agent/src/outline.rs b/crates/agent/src/outline.rs index 262fa8d3d139a5c8f5900d0dd55348f9dc716167..0de035c34bf285d41ff20676f037abf2464213a1 100644 --- a/crates/agent/src/outline.rs +++ b/crates/agent/src/outline.rs @@ -44,6 +44,25 @@ pub async fn get_buffer_content_or_outline( .collect::>() })?; + // If no outline exists, fall back to first 1KB so the agent has some context + if outline_items.is_empty() { + let text = buffer.read_with(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let len = snapshot.len().min(1024); + let content = snapshot.text_for_range(0..len).collect::(); + if let Some(path) = path { + format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}") + } else { + format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}") + } + })?; + + return Ok(BufferContent { + text, + is_outline: false, + }); + } + let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; let text = if let Some(path) = path { @@ -140,3 +159,62 @@ fn render_entries( entries_rendered } + +#[cfg(test)] +mod tests { + use super::*; + use fs::FakeFs; + use gpui::TestAppContext; + use project::Project; + use settings::SettingsStore; + + #[gpui::test] + async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + let content = "A".repeat(100 * 1024); // 100KB + let content_len = content.len(); + let buffer = project + .update(cx, |project, cx| project.create_buffer(true, cx)) + .await + .expect("failed to create buffer"); + + buffer.update(cx, |buffer, cx| buffer.set_text(content, cx)); + + let result = cx + .spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await }) + .await + .unwrap(); + + // Should contain some of the actual file content + assert!( + result.text.contains("AAAAAAAAAA"), + "Result did not contain content subset" + ); + + // Should be marked as not an outline (it's truncated content) + assert!( + !result.is_outline, + "Large file without outline should not be marked as outline" + ); + + // Should be reasonably sized (much smaller than original) + assert!( + result.text.len() < 50 * 1024, + "Result size {} should be smaller than 50KB", + result.text.len() + ); + + // Should be significantly smaller than the original content + assert!( + result.text.len() < content_len / 10, + "Result should be much smaller than original content" + ); + } +} diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index b7037a6413d93fb4ee538af7062049df9f58e818..9835dc929bd86085b481cbdb5e2ee667591c6e73 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -2671,13 +2671,14 @@ mod tests { } #[gpui::test] - async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) { + async fn test_large_file_mention_fallback(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); // Create a large file that exceeds AUTO_OUTLINE_SIZE - const LINE: &str = "fn example_function() { /* some code */ }\n"; + // Using plain text without a configured language, so no outline is available + const LINE: &str = "This is a line of text in the file\n"; let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len())); assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE); @@ -2688,8 +2689,8 @@ mod tests { fs.insert_tree( "/project", json!({ - "large_file.rs": large_content.clone(), - "small_file.rs": small_content, + "large_file.txt": large_content.clone(), + "small_file.txt": small_content, }), ) .await; @@ -2735,7 +2736,7 @@ mod tests { let large_file_abs_path = project.read_with(cx, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("large_file.rs") + worktree_root.join("large_file.txt") }); let large_file_task = message_editor.update(cx, |editor, cx| { editor.confirm_mention_for_file(large_file_abs_path, cx) @@ -2744,11 +2745,20 @@ mod tests { let large_file_mention = large_file_task.await.unwrap(); match large_file_mention { Mention::Text { content, .. } => { - // Should contain outline header for large files - assert!(content.contains("File outline for")); - assert!(content.contains("file too large to show full content")); - // Should not contain the full repeated content - assert!(!content.contains(&LINE.repeat(100))); + // Should contain some of the content but not all of it + assert!( + content.contains(LINE), + "Should contain some of the file content" + ); + assert!( + !content.contains(&LINE.repeat(100)), + "Should not contain the full file" + ); + // Should be much smaller than original + assert!( + content.len() < large_content.len() / 10, + "Should be significantly truncated" + ); } _ => panic!("Expected Text mention for large file"), } @@ -2758,7 +2768,7 @@ mod tests { let small_file_abs_path = project.read_with(cx, |project, cx| { let worktree = project.worktrees(cx).next().unwrap(); let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("small_file.rs") + worktree_root.join("small_file.txt") }); let small_file_task = message_editor.update(cx, |editor, cx| { editor.confirm_mention_for_file(small_file_abs_path, cx) @@ -2767,10 +2777,8 @@ mod tests { let small_file_mention = small_file_task.await.unwrap(); match small_file_mention { Mention::Text { content, .. } => { - // Should contain the actual content + // Should contain the full actual content assert_eq!(content, small_content); - // Should not contain outline header - assert!(!content.contains("File outline for")); } _ => panic!("Expected Text mention for small file"), } diff --git a/crates/agent_ui/src/context.rs b/crates/agent_ui/src/context.rs index 0bbf4d45ee56bf8220987f52fd7a1f6aa0a73055..7f497f9cab9eae7ca9fa2a573100ab2993546228 100644 --- a/crates/agent_ui/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -1089,7 +1089,7 @@ mod tests { } #[gpui::test] - async fn test_large_file_uses_outline(cx: &mut TestAppContext) { + async fn test_large_file_uses_fallback(cx: &mut TestAppContext) { init_test_settings(cx); // Create a large file that exceeds AUTO_OUTLINE_SIZE @@ -1101,16 +1101,16 @@ mod tests { let file_context = load_context_for("file.txt", large_content, cx).await; + // Should contain some of the actual file content assert!( - file_context - .text - .contains(&format!("# File outline for {}", path!("test/file.txt"))), - "Large files should not get an outline" + file_context.text.contains(LINE), + "Should contain some of the file content" ); + // Should be much smaller than original assert!( - file_context.text.len() < content_len, - "Outline should be smaller than original content" + file_context.text.len() < content_len / 10, + "Should be significantly smaller than original content" ); }