This commit is contained in:
Morgan VanYperen 2025-07-06 23:49:59 -04:00 committed by GitHub
commit 9b7b688a37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 162 additions and 4 deletions

View file

@ -63,10 +63,30 @@ export const ReadTool = Tool.define({
const limit = params.limit ?? DEFAULT_READ_LIMIT
const offset = params.offset || 0
const isImage = isImageFile(filePath)
if (isImage)
throw new Error(
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
)
if (isImage) {
// Handle image files by returning them as base64 data URL
const buffer = await file.arrayBuffer()
const base64 = Buffer.from(buffer).toString("base64")
const mimeType = getMimeType(isImage)
const dataUrl = `data:${mimeType};base64,${base64}`
const output = `<image>\n${dataUrl}\n</image>`
const preview = `Image file: ${path.basename(filePath)} (${isImage})`
// just warms the lsp client
await LSP.touchFile(filePath, true)
FileTime.read(ctx.sessionID, filePath)
return {
output,
metadata: {
preview,
title: path.relative(App.info().path.root, filePath),
},
}
}
const lines = await file.text().then((text) => text.split("\n"))
const raw = lines.slice(offset, offset + limit).map((line) => {
return line.length > MAX_LINE_LENGTH
@ -122,3 +142,22 @@ function isImageFile(filePath: string): string | false {
return false
}
}
function getMimeType(imageType: string): string {
switch (imageType) {
case "JPEG":
return "image/jpeg"
case "PNG":
return "image/png"
case "GIF":
return "image/gif"
case "BMP":
return "image/bmp"
case "SVG":
return "image/svg+xml"
case "WebP":
return "image/webp"
default:
return "application/octet-stream"
}
}

View file

@ -0,0 +1,119 @@
import { describe, expect, test } from "bun:test"
import { App } from "../../src/app/app"
import { ReadTool } from "../../src/tool/read"
import * as fs from "fs"
import * as path from "path"
const ctx = {
sessionID: "test",
messageID: "",
abort: AbortSignal.any([]),
metadata: () => {},
}
describe("tool.read", () => {
test("read text file", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
const result = await ReadTool.execute(
{
filePath: path.join(process.cwd(), "README.md"),
},
ctx,
)
expect(result.output).toContain("<file>")
expect(result.output).toContain("</file>")
expect(result.metadata.title).toBe("README.md")
})
})
test("read PNG file", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
// Create a minimal PNG file for testing
const testPngPath = path.join(process.cwd(), "test-image.png")
// 1x1 red pixel PNG
const pngData = Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00,
0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xdd, 0x8d, 0xb4, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
])
fs.writeFileSync(testPngPath, pngData)
try {
const result = await ReadTool.execute(
{
filePath: testPngPath,
},
ctx,
)
expect(result.output).toContain("<image>")
expect(result.output).toContain("data:image/png;base64,")
expect(result.output).toContain("</image>")
expect(result.metadata.preview).toContain(
"Image file: test-image.png (PNG)",
)
expect(result.metadata.title).toBe("test-image.png")
} finally {
// Clean up test file
fs.unlinkSync(testPngPath)
}
})
})
test("read JPEG file", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
// Create a minimal JPEG file for testing
const testJpegPath = path.join(process.cwd(), "test-image.jpg")
// Minimal JPEG structure
const jpegData = Buffer.from([
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43,
0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09,
0x09, 0x08, 0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12,
0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a, 0x1c, 0x1c, 0x20,
0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29,
0x2c, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32,
0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xd9,
])
fs.writeFileSync(testJpegPath, jpegData)
try {
const result = await ReadTool.execute(
{
filePath: testJpegPath,
},
ctx,
)
expect(result.output).toContain("<image>")
expect(result.output).toContain("data:image/jpeg;base64,")
expect(result.output).toContain("</image>")
expect(result.metadata.preview).toContain(
"Image file: test-image.jpg (JPEG)",
)
expect(result.metadata.title).toBe("test-image.jpg")
} finally {
// Clean up test file
fs.unlinkSync(testJpegPath)
}
})
})
test("file not found", async () => {
await App.provide({ cwd: process.cwd() }, async () => {
await expect(
ReadTool.execute(
{
filePath: "/tmp/nonexistent-file-that-does-not-exist.txt",
},
ctx,
),
).rejects.toThrow("File not found")
})
})
})