agent: Return clear error when read_file tool path is a directory (#54303)

Prohect and Copilot Autofix powered by AI created

Fixes #54244

When the `read_file` tool is called with a path that points to a
directory instead of a file, it now returns a clear, actionable error
message telling the agent to use `list_directory` instead.

Previously the tool would fail with an unhelpful generic error. Now it
explicitly checks whether the path is a directory before attempting to
read it.

A test covering this case is also included.

Release Notes:

- Fixed `read_file` tool returning an unhelpful error when given a
directory path; it now suggests using `list_directory` instead.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Change summary

crates/agent/src/tools/read_file_tool.rs | 40 ++++++++++++++++++++++++++
1 file changed, 40 insertions(+)

Detailed changes

crates/agent/src/tools/read_file_tool.rs 🔗

@@ -184,6 +184,13 @@ impl AgentTool for ReadFileTool {
                 anyhow::Ok(())
             }).map_err(tool_content_err)?;
 
+            if fs.is_dir(&abs_path).await {
+                return Err(tool_content_err(format!(
+                    "{} is a directory, not a file. Use the list_directory tool to explore directory contents.",
+                    &input.path
+                )));
+            }
+
             if let Some(canonical_target) = &symlink_canonical_target {
                 let authorize = cx.update(|cx| {
                     authorize_symlink_access(
@@ -356,6 +363,39 @@ mod test {
     use std::sync::Arc;
     use util::path;
 
+    #[gpui::test]
+    async fn test_read_directory_path(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            path!("/root"),
+            json!({
+                "some_dir": {}
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+        let tool = Arc::new(ReadFileTool::new(project, action_log, true));
+        let (event_stream, _) = ToolCallEventStream::test();
+
+        let result = cx
+            .update(|cx| {
+                let input = ReadFileToolInput {
+                    path: "root/some_dir".to_string(),
+                    start_line: None,
+                    end_line: None,
+                };
+                tool.run(ToolInput::resolved(input), event_stream, cx)
+            })
+            .await;
+        assert_eq!(
+            error_text(result.unwrap_err()),
+            "root/some_dir is a directory, not a file. Use the list_directory tool to explore directory contents."
+        );
+    }
+
     #[gpui::test]
     async fn test_read_nonexistent_file(cx: &mut TestAppContext) {
         init_test(cx);