feat: adjust read tool so that it can handle dirs too (#13090)

This commit is contained in:
Aiden Cline
2026-02-11 13:23:00 -06:00
committed by GitHub
parent e3471526f4
commit 6b4d617df0
5 changed files with 115 additions and 22 deletions

View File

@@ -17,9 +17,9 @@ const MAX_BYTES = 50 * 1024
export const ReadTool = Tool.define("read", {
description: DESCRIPTION,
parameters: z.object({
filePath: z.string().describe("The path to the file to read"),
offset: z.coerce.number().describe("The line number to start reading from (0-based)").optional(),
limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
filePath: z.string().describe("The absolute path to the file or directory to read"),
offset: z.coerce.number().describe("The 0-based line offset to start reading from").optional(),
limit: z.coerce.number().describe("The maximum number of lines to read (defaults to 2000)").optional(),
}),
async execute(params, ctx) {
let filepath = params.filePath
@@ -28,8 +28,12 @@ export const ReadTool = Tool.define("read", {
}
const title = path.relative(Instance.worktree, filepath)
const file = Bun.file(filepath)
const stat = await file.stat().catch(() => undefined)
await assertExternalDirectory(ctx, filepath, {
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
kind: stat?.isDirectory() ? "directory" : "file",
})
await ctx.ask({
@@ -39,8 +43,7 @@ export const ReadTool = Tool.define("read", {
metadata: {},
})
const file = Bun.file(filepath)
if (!(await file.exists())) {
if (!stat) {
const dir = path.dirname(filepath)
const base = path.basename(filepath)
@@ -60,6 +63,47 @@ export const ReadTool = Tool.define("read", {
throw new Error(`File not found: ${filepath}`)
}
if (stat.isDirectory()) {
const dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
const entries = await Promise.all(
dirents.map(async (dirent) => {
if (dirent.isDirectory()) return dirent.name + "/"
if (dirent.isSymbolicLink()) {
const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined)
if (target?.isDirectory()) return dirent.name + "/"
}
return dirent.name
}),
)
entries.sort((a, b) => a.localeCompare(b))
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const sliced = entries.slice(offset, offset + limit)
const truncated = offset + sliced.length < entries.length
const output = [
`<path>${filepath}</path>`,
`<type>directory</type>`,
`<entries>`,
sliced.join("\n"),
truncated
? `\n(Showing ${sliced.length} of ${entries.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})`
: `\n(${entries.length} entries)`,
`</entries>`,
].join("\n")
return {
title,
output,
metadata: {
preview: sliced.slice(0, 20).join("\n"),
truncated,
loaded: [] as string[],
},
}
}
const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
// Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files)
@@ -75,7 +119,7 @@ export const ReadTool = Tool.define("read", {
metadata: {
preview: msg,
truncated: false,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
loaded: instructions.map((i) => i.filepath),
},
attachments: [
{
@@ -112,11 +156,11 @@ export const ReadTool = Tool.define("read", {
}
const content = raw.map((line, index) => {
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
return `${index + offset + 1}: ${line}`
})
const preview = raw.slice(0, 20).join("\n")
let output = "<file>\n"
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
output += content.join("\n")
const totalLines = lines.length
@@ -131,7 +175,7 @@ export const ReadTool = Tool.define("read", {
} else {
output += `\n\n(End of file - total ${totalLines} lines)`
}
output += "\n</file>"
output += "\n</content>"
// just warms the lsp client
LSP.touchFile(filepath, false)
@@ -147,7 +191,7 @@ export const ReadTool = Tool.define("read", {
metadata: {
preview,
truncated,
...(instructions.length > 0 && { loaded: instructions.map((i) => i.filepath) }),
loaded: instructions.map((i) => i.filepath),
},
}
},