summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/webfetch.ts28
-rw-r--r--packages/opencode/test/tool/webfetch.test.ts97
2 files changed, 123 insertions, 2 deletions
diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts
index c9479b9df..cd0d8dcde 100644
--- a/packages/opencode/src/tool/webfetch.ts
+++ b/packages/opencode/src/tool/webfetch.ts
@@ -3,6 +3,7 @@ import { Tool } from "./tool"
import TurndownService from "turndown"
import DESCRIPTION from "./webfetch.txt"
import { abortAfterAny } from "../util/abort"
+import { Identifier } from "../id/id"
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
@@ -87,11 +88,34 @@ export const WebFetchTool = Tool.define("webfetch", {
throw new Error("Response too large (exceeds 5MB limit)")
}
- const content = new TextDecoder().decode(arrayBuffer)
const contentType = response.headers.get("content-type") || ""
-
+ const mime = contentType.split(";")[0]?.trim().toLowerCase() || ""
const title = `${params.url} (${contentType})`
+ // Check if response is an image
+ const isImage = mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet"
+
+ if (isImage) {
+ const base64Content = Buffer.from(arrayBuffer).toString("base64")
+ return {
+ title,
+ output: "Image fetched successfully",
+ metadata: {},
+ attachments: [
+ {
+ id: Identifier.ascending("part"),
+ sessionID: ctx.sessionID,
+ messageID: ctx.messageID,
+ type: "file",
+ mime,
+ url: `data:${mime};base64,${base64Content}`,
+ },
+ ],
+ }
+ }
+
+ const content = new TextDecoder().decode(arrayBuffer)
+
// Handle content based on requested format and actual content type
switch (params.format) {
case "markdown":
diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts
new file mode 100644
index 000000000..10178af8f
--- /dev/null
+++ b/packages/opencode/test/tool/webfetch.test.ts
@@ -0,0 +1,97 @@
+import { describe, expect, test } from "bun:test"
+import path from "path"
+import { Instance } from "../../src/project/instance"
+import { WebFetchTool } from "../../src/tool/webfetch"
+
+const projectRoot = path.join(import.meta.dir, "../..")
+
+const ctx = {
+ sessionID: "test",
+ messageID: "message",
+ callID: "",
+ agent: "build",
+ abort: AbortSignal.any([]),
+ messages: [],
+ metadata: () => {},
+ ask: async () => {},
+}
+
+async function withFetch(
+ mockFetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>,
+ fn: () => Promise<void>,
+) {
+ const originalFetch = globalThis.fetch
+ globalThis.fetch = mockFetch as unknown as typeof fetch
+ try {
+ await fn()
+ } finally {
+ globalThis.fetch = originalFetch
+ }
+}
+
+describe("tool.webfetch", () => {
+ test("returns image responses as file attachments", async () => {
+ const bytes = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])
+ await withFetch(
+ async () => new Response(bytes, { status: 200, headers: { "content-type": "IMAGE/PNG; charset=binary" } }),
+ async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const webfetch = await WebFetchTool.init()
+ const result = await webfetch.execute({ url: "https://example.com/image.png", format: "markdown" }, ctx)
+ expect(result.output).toBe("Image fetched successfully")
+ expect(result.attachments).toBeDefined()
+ expect(result.attachments?.length).toBe(1)
+ expect(result.attachments?.[0].type).toBe("file")
+ expect(result.attachments?.[0].mime).toBe("image/png")
+ expect(result.attachments?.[0].url.startsWith("data:image/png;base64,")).toBe(true)
+ },
+ })
+ },
+ )
+ })
+
+ test("keeps svg as text output", async () => {
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg"><text>hello</text></svg>'
+ await withFetch(
+ async () =>
+ new Response(svg, {
+ status: 200,
+ headers: { "content-type": "image/svg+xml; charset=UTF-8" },
+ }),
+ async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const webfetch = await WebFetchTool.init()
+ const result = await webfetch.execute({ url: "https://example.com/image.svg", format: "html" }, ctx)
+ expect(result.output).toContain("<svg")
+ expect(result.attachments).toBeUndefined()
+ },
+ })
+ },
+ )
+ })
+
+ test("keeps text responses as text output", async () => {
+ await withFetch(
+ async () =>
+ new Response("hello from webfetch", {
+ status: 200,
+ headers: { "content-type": "text/plain; charset=utf-8" },
+ }),
+ async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ const webfetch = await WebFetchTool.init()
+ const result = await webfetch.execute({ url: "https://example.com/file.txt", format: "text" }, ctx)
+ expect(result.output).toBe("hello from webfetch")
+ expect(result.attachments).toBeUndefined()
+ },
+ })
+ },
+ )
+ })
+})