Gracefully handle `@mention`-ing large files with no outlines (#42543)

Richard Feldman created

Closes #32098

Release Notes:

- In the Agent panel, when `@mention`-ing large files with no outline,
their first 1KB is now added to context

Change summary

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(-)

Detailed changes

crates/agent/src/outline.rs 🔗

@@ -44,6 +44,25 @@ pub async fn get_buffer_content_or_outline(
                 .collect::<Vec<_>>()
         })?;
 
+        // 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::<String>();
+                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"
+        );
+    }
+}

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"),
         }

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"
         );
     }