ソースを参照

chore: generate

opencode-agent[bot] 2 ヶ月 前
コミット
cc52dc396c

+ 1 - 4
packages/core/src/filesystem/search.ts

@@ -147,10 +147,7 @@ function collectPaths<T>(
     return [{ ...result, score: scores[index]?.total ?? 0 }]
   })
   rows.sort(
-    (a, b) =>
-      b.score - a.score ||
-      a.path.length - b.path.length ||
-      (a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
+    (a, b) => b.score - a.score || a.path.length - b.path.length || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
   )
 
   const seen = new Set<string>()

+ 4 - 1
packages/core/src/image.ts

@@ -35,7 +35,10 @@ export interface Interface {
   readonly normalize: (
     resource: string,
     content: FileSystem.Content & { readonly encoding: "base64" },
-  ) => Effect.Effect<FileSystem.Content & { readonly encoding: "base64" }, ResizerUnavailableError | DecodeError | SizeError>
+  ) => Effect.Effect<
+    FileSystem.Content & { readonly encoding: "base64" },
+    ResizerUnavailableError | DecodeError | SizeError
+  >
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Image") {}

+ 2 - 1
packages/core/src/location-search.ts

@@ -112,7 +112,8 @@ export const layer = Layer.effect(
       const root = yield* fs.realPath(directory).pipe(Effect.orDie)
       if (!FSUtil.contains(root, real)) return yield* Effect.die(new globalThis.Error("Path escapes the search root"))
       const info = yield* fs.stat(real).pipe(Effect.orDie)
-      const type = info.type === "File" ? ("file" as const) : info.type === "Directory" ? ("directory" as const) : undefined
+      const type =
+        info.type === "File" ? ("file" as const) : info.type === "Directory" ? ("directory" as const) : undefined
       if (!type) return yield* Effect.die(new globalThis.Error("Search root is not a file or directory"))
       return { real, root, resource: slash(path.relative(root, real)) || ".", type }
     })

+ 1 - 7
packages/core/src/session/runner/publish-llm-event.ts

@@ -1,10 +1,4 @@
-import {
-  ToolOutput,
-  type LLMEvent,
-  type ProviderMetadata,
-  type ToolResultValue,
-  type Usage,
-} from "@opencode-ai/llm"
+import { ToolOutput, type LLMEvent, type ProviderMetadata, type ToolResultValue, type Usage } from "@opencode-ai/llm"
 import { DateTime, Effect } from "effect"
 import { EventV2 } from "../../event"
 import { ModelV2 } from "../../model"

+ 41 - 11
packages/core/src/tool/read-filesystem.ts

@@ -53,16 +53,45 @@ export class ListPage extends Schema.Class<ListPage>("ReadTool.ListPage")({
 
 export interface Interface {
   readonly inspect: (path: AbsolutePath) => Effect.Effect<"file" | "directory">
-  readonly read: (path: AbsolutePath, resource: string, page?: PageInput) => Effect.Effect<FileSystem.Content | TextPage>
+  readonly read: (
+    path: AbsolutePath,
+    resource: string,
+    page?: PageInput,
+  ) => Effect.Effect<FileSystem.Content | TextPage>
   readonly list: (path: AbsolutePath, page?: PageInput) => Effect.Effect<ListPage>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ReadToolFileSystem") {}
 
 const extensions = new Set([
-  ".zip", ".tar", ".gz", ".exe", ".dll", ".so", ".class", ".jar", ".war", ".7z", ".doc", ".docx",
-  ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp", ".bin", ".dat", ".obj", ".o", ".a",
-  ".lib", ".wasm", ".pyc", ".pyo",
+  ".zip",
+  ".tar",
+  ".gz",
+  ".exe",
+  ".dll",
+  ".so",
+  ".class",
+  ".jar",
+  ".war",
+  ".7z",
+  ".doc",
+  ".docx",
+  ".xls",
+  ".xlsx",
+  ".ppt",
+  ".pptx",
+  ".odt",
+  ".ods",
+  ".odp",
+  ".bin",
+  ".dat",
+  ".obj",
+  ".o",
+  ".a",
+  ".lib",
+  ".wasm",
+  ".pyc",
+  ".pyo",
 ])
 const startsWith = (bytes: Uint8Array, prefix: number[]) => prefix.every((value, index) => bytes[index] === value)
 const imageMime = (bytes: Uint8Array) => {
@@ -113,7 +142,9 @@ export const read = Effect.fn("ReadTool.read")(function* (
         const chunks = [first]
         let total = first.length
         while (total <= MAX_MEDIA_INGEST_BYTES) {
-          const chunk = yield* file.readAlloc(Math.min(64 * 1024, MAX_MEDIA_INGEST_BYTES + 1 - total)).pipe(Effect.orDie)
+          const chunk = yield* file
+            .readAlloc(Math.min(64 * 1024, MAX_MEDIA_INGEST_BYTES + 1 - total))
+            .pipe(Effect.orDie)
           if (Option.isNone(chunk)) break
           chunks.push(chunk.value)
           total += chunk.value.length
@@ -123,7 +154,10 @@ export const read = Effect.fn("ReadTool.read")(function* (
         return {
           uri: pathToFileURL(real).href,
           name: path.basename(real),
-          content: Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total).toString("base64"),
+          content: Buffer.concat(
+            chunks.map((chunk) => Buffer.from(chunk)),
+            total,
+          ).toString("base64"),
           encoding: "base64" as const,
           mime,
         }
@@ -226,11 +260,7 @@ export const read = Effect.fn("ReadTool.read")(function* (
   )
 })
 
-export const list = Effect.fn("ReadTool.list")(function* (
-  fs: FSUtil.Interface,
-  input: string,
-  page: PageInput = {},
-) {
+export const list = Effect.fn("ReadTool.list")(function* (fs: FSUtil.Interface, input: string, page: PageInput = {}) {
   const real = yield* fs.realPath(input).pipe(Effect.orDie)
   const items = yield* fs.readDirectoryEntries(real).pipe(Effect.orDie)
   const offset = page.offset ?? 1

+ 3 - 3
packages/core/src/tool/read.ts

@@ -59,7 +59,8 @@ export const layer = Layer.effectDiscard(
                 return yield* Effect.die(new Error("Path escapes the allowed read root"))
               const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
               const root = yield* fs.realPath(selected).pipe(Effect.orDie)
-              if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the allowed read root"))
+              if (!FSUtil.contains(root, real))
+                return yield* Effect.die(new Error("Path escapes the allowed read root"))
               const resource = path.relative(root, real).replaceAll("\\", "/") || "."
               const target = AbsolutePath.make(real)
               const type = yield* reader.inspect(target)
@@ -71,8 +72,7 @@ export const layer = Layer.effectDiscard(
                 agent: context.agent,
                 source: { type: "tool", messageID: context.assistantMessageID, callID: context.toolCallID },
               })
-              if (type === "directory")
-                return yield* reader.list(target, { offset: input.offset, limit: input.limit })
+              if (type === "directory") return yield* reader.list(target, { offset: input.offset, limit: input.limit })
               const content = yield* reader.read(target, resource, {
                 offset: input.offset,
                 limit: input.limit,

+ 6 - 8
packages/core/src/tool/tool.ts

@@ -92,11 +92,10 @@ export function make<Input extends SchemaType<any>, Output extends SchemaType<an
                 ),
               ),
             ),
-            Effect.map((output) =>
-              ({
-                structured: output,
-                content:
-                  config.toModelOutput?.({ input, output }).map((part) =>
+            Effect.map((output) => ({
+              structured: output,
+              content:
+                config.toModelOutput?.({ input, output }).map((part) =>
                   part.type === "text"
                     ? { type: "text" as const, text: part.text }
                     : {
@@ -105,9 +104,8 @@ export function make<Input extends SchemaType<any>, Output extends SchemaType<an
                         mime: part.mime,
                         name: part.name,
                       },
-                  ) ?? (typeof output === "string" ? [{ type: "text" as const, text: output }] : []),
-              }),
-            ),
+                ) ?? (typeof output === "string" ? [{ type: "text" as const, text: output }] : []),
+            })),
           ),
         ),
       ),

+ 12 - 4
packages/core/test/location-filesystem.test.ts

@@ -73,8 +73,12 @@ describe("FileSystem", () => {
           { path: RelativePath.make("src"), type: "directory", mime: "application/x-directory" },
           { path: RelativePath.make("README.md"), type: "file", mime: "text/markdown" },
         ])
-        expect(yield* Effect.promise(() => Promise.all(entries.map((entry) => fs.realpath(fileURLToPath(entry.uri)))))).toEqual(
-          yield* Effect.promise(() => Promise.all([fs.realpath(path.join(directory, "src")), fs.realpath(path.join(directory, "README.md"))])),
+        expect(
+          yield* Effect.promise(() => Promise.all(entries.map((entry) => fs.realpath(fileURLToPath(entry.uri))))),
+        ).toEqual(
+          yield* Effect.promise(() =>
+            Promise.all([fs.realpath(path.join(directory, "src")), fs.realpath(path.join(directory, "README.md"))]),
+          ),
         )
       }).pipe(provide(directory)),
     ),
@@ -84,12 +88,16 @@ describe("FileSystem", () => {
     withTmp((directory) =>
       Effect.gen(function* () {
         const service = yield* FileSystem.Service
-        expect(Exit.isFailure(yield* service.read({ path: RelativePath.make("../outside.txt") }).pipe(Effect.exit))).toBe(true)
+        expect(
+          Exit.isFailure(yield* service.read({ path: RelativePath.make("../outside.txt") }).pipe(Effect.exit)),
+        ).toBe(true)
         if (process.platform === "win32") return
         const outside = `${directory}-outside.txt`
         yield* Effect.promise(() => fs.writeFile(outside, "outside"))
         yield* Effect.promise(() => fs.symlink(outside, path.join(directory, "link.txt")))
-        expect(Exit.isFailure(yield* service.read({ path: RelativePath.make("link.txt") }).pipe(Effect.exit))).toBe(true)
+        expect(Exit.isFailure(yield* service.read({ path: RelativePath.make("link.txt") }).pipe(Effect.exit))).toBe(
+          true,
+        )
         yield* Effect.promise(() => fs.rm(outside, { force: true }))
       }).pipe(provide(directory)),
     ),

+ 1 - 5
packages/core/test/tool-glob.test.ts

@@ -43,11 +43,7 @@ const search = Layer.succeed(
 )
 
 const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
-const glob = GlobTool.layer.pipe(
-  Layer.provide(registry),
-  Layer.provide(permission),
-  Layer.provide(search),
-)
+const glob = GlobTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(search))
 const it = testEffect(Layer.mergeAll(registry, permission, search, glob))
 
 const reset = () => {

+ 1 - 5
packages/core/test/tool-grep.test.ts

@@ -54,11 +54,7 @@ const permission = Layer.succeed(
   }),
 )
 const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
-const grep = GrepTool.layer.pipe(
-  Layer.provide(registry),
-  Layer.provide(search),
-  Layer.provide(permission),
-)
+const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(search), Layer.provide(permission))
 const it = testEffect(Layer.mergeAll(registry, search, permission, grep))
 const sessionID = SessionV2.ID.make("ses_grep_tool_test")
 

+ 2 - 7
packages/core/test/tool-read.test.ts

@@ -37,10 +37,7 @@ let configEntries: Config.Entry[] = []
 const reader = Layer.succeed(
   ReadToolFileSystem.Service,
   ReadToolFileSystem.Service.of({
-    inspect: () =>
-      resolveFailure === undefined
-        ? Effect.succeed(resolvedType)
-        : Effect.die(resolveFailure),
+    inspect: () => (resolveFailure === undefined ? Effect.succeed(resolvedType) : Effect.die(resolveFailure)),
     read: (input, _resource, page = {}) => {
       readCalls.push({ input, page })
       if (readFailure !== undefined) return Effect.die(readFailure)
@@ -73,9 +70,7 @@ const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () =>
 const image = Image.layer.pipe(Layer.provide(config))
 const testFileSystem = Layer.effect(
   FSUtil.Service,
-  FSUtil.Service.use((fs) =>
-    Effect.succeed(FSUtil.Service.of({ ...fs, realPath: (path) => Effect.succeed(path) })),
-  ),
+  FSUtil.Service.use((fs) => Effect.succeed(FSUtil.Service.of({ ...fs, realPath: (path) => Effect.succeed(path) }))),
 ).pipe(Layer.provide(FSUtil.defaultLayer))
 const infrastructure = Layer.mergeAll(
   testFileSystem,

+ 6 - 1
packages/llm/src/protocols/bedrock-converse.ts

@@ -269,7 +269,12 @@ const lowerToolResultContent = Effect.fn("BedrockConverse.lowerToolResultContent
       content.push({ text: item.text })
       continue
     }
-    const media = yield* BedrockMedia.lower({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
+    const media = yield* BedrockMedia.lower({
+      type: "media",
+      mediaType: item.mime,
+      data: item.uri,
+      filename: item.name,
+    })
     if (!("image" in media))
       return yield* ProviderShared.invalidRequest("Bedrock Converse only supports image media in tool results")
     content.push(media)

+ 1 - 3
packages/llm/src/protocols/openai-chat.ts

@@ -200,9 +200,7 @@ const lowerToolCall = (part: ToolCallPart): OpenAIChatAssistantToolCall => ({
   },
 })
 
-const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (
-  part: MediaPart,
-) {
+const lowerMedia = Effect.fn("OpenAIChat.lowerMedia")(function* (part: MediaPart) {
   const media = yield* ProviderShared.validateMedia("OpenAI Chat", part, IMAGE_MIMES)
   return { type: "image_url" as const, image_url: { url: media.dataUrl } }
 })

+ 8 - 2
packages/llm/test/provider/openai-chat.test.ts

@@ -327,12 +327,18 @@ describe("OpenAI Chat route", () => {
             Message.tool({
               id: "call_1",
               name: "read",
-              result: { type: "content", value: [{ type: "file", uri: "data:image/png;base64,AAEC", mime: "image/png" }] },
+              result: {
+                type: "content",
+                value: [{ type: "file", uri: "data:image/png;base64,AAEC", mime: "image/png" }],
+              },
             }),
             Message.tool({
               id: "call_2",
               name: "read",
-              result: { type: "content", value: [{ type: "file", uri: "data:image/webp;base64,UklG", mime: "image/webp" }] },
+              result: {
+                type: "content",
+                value: [{ type: "file", uri: "data:image/webp;base64,UklG", mime: "image/webp" }],
+              },
             }),
             Message.system("Inspect both images."),
           ],

+ 5 - 15
packages/llm/test/tool-runtime.test.ts

@@ -241,16 +241,12 @@ describe("LLMClient tools", () => {
         uri: "data:image/png;base64,AAAA",
         mime: "image/png",
       })
-      expect(
-        decode({ type: "file", uri: "https://example.test/image.png", mime: "image/png" }),
-      ).toEqual({
+      expect(decode({ type: "file", uri: "https://example.test/image.png", mime: "image/png" })).toEqual({
         type: "file",
         uri: "https://example.test/image.png",
         mime: "image/png",
       })
-      expect(
-        decode({ type: "file", uri: "file:///tmp/image.png", mime: "image/png" }),
-      ).toEqual({
+      expect(decode({ type: "file", uri: "file:///tmp/image.png", mime: "image/png" })).toEqual({
         type: "file",
         uri: "file:///tmp/image.png",
         mime: "image/png",
@@ -270,9 +266,7 @@ describe("LLMClient tools", () => {
       })
       expect(
         ToolOutput.toResultValue(
-          ToolOutput.make({}, [
-            { type: "file", uri: "https://example.test/image.png", mime: "image/png" },
-          ]),
+          ToolOutput.make({}, [{ type: "file", uri: "https://example.test/image.png", mime: "image/png" }]),
         ),
       ).toEqual({
         type: "content",
@@ -280,9 +274,7 @@ describe("LLMClient tools", () => {
       })
       expect(
         ToolOutput.toResultValue(
-          ToolOutput.make({}, [
-            { type: "file", uri: "file:///tmp/image.png", mime: "image/png" },
-          ]),
+          ToolOutput.make({}, [{ type: "file", uri: "file:///tmp/image.png", mime: "image/png" }]),
         ),
       ).toEqual({
         type: "content",
@@ -307,9 +299,7 @@ describe("LLMClient tools", () => {
         parameters: Schema.Struct({}),
         success: Schema.Struct({ ok: Schema.Boolean }),
         execute: () => Effect.succeed({ ok: true }),
-        toModelOutput: () => [
-          { type: "file", uri: "https://example.test/image.png", mime: "image/png" },
-        ],
+        toModelOutput: () => [{ type: "file", uri: "https://example.test/image.png", mime: "image/png" }],
       })
 
       const dispatched = yield* ToolRuntime.dispatch(

+ 1 - 3
packages/opencode/script/bench-search.ts

@@ -91,9 +91,7 @@ console.log("--- Search service (warm) ---")
 for (const q of FILE_QUERIES) {
   const t = performance.now()
   const r = await run(Search.Service.use((svc) => svc.file({ cwd: dir, query: q, limit: FILE_LIMIT })))
-  console.log(
-    `[Search.file] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} results)`,
-  )
+  console.log(`[Search.file] "${q}": ${(performance.now() - t).toFixed(1)}ms (${r.length} results)`)
 }
 
 for (const q of GREP_QUERIES) {

+ 2 - 6
packages/opencode/src/cli/cmd/debug/file.ts

@@ -38,9 +38,7 @@ const FileReadCommand = effectCmd({
       description: "File path to read",
     }),
   handler: Effect.fn("Cli.debug.file.read")(function* (args) {
-    const content = yield* filesystem(
-      FileSystem.Service.use((svc) => svc.read({ path: RelativePath.make(args.path) })),
-    )
+    const content = yield* filesystem(FileSystem.Service.use((svc) => svc.read({ path: RelativePath.make(args.path) })))
     process.stdout.write(JSON.stringify(content, null, 2) + EOL)
   }),
 })
@@ -55,9 +53,7 @@ const FileListCommand = effectCmd({
       description: "File path to list",
     }),
   handler: Effect.fn("Cli.debug.file.list")(function* (args) {
-    const files = yield* filesystem(
-      FileSystem.Service.use((svc) => svc.list({ path: RelativePath.make(args.path) })),
-    )
+    const files = yield* filesystem(FileSystem.Service.use((svc) => svc.list({ path: RelativePath.make(args.path) })))
     process.stdout.write(JSON.stringify(files, null, 2) + EOL)
   }),
 })

+ 8 - 7
packages/opencode/src/session/processor.ts

@@ -595,13 +595,14 @@ export const layer = Layer.effect(
               const assistantMessageID = yield* requireV2AssistantMessage(toolCall?.call)
               const content = [
                 { type: "text" as const, text: output.output },
-                ...(output.attachments?.map((item: SessionV1.FilePart) =>
-                  ({
-                    type: "file",
-                    uri: item.url,
-                    mime: item.mime,
-                    name: item.filename,
-                  }) as const,
+                ...(output.attachments?.map(
+                  (item: SessionV1.FilePart) =>
+                    ({
+                      type: "file",
+                      uri: item.url,
+                      mime: item.mime,
+                      name: item.filename,
+                    }) as const,
                 ) ?? []),
               ]
               const unsupported = content.find((item) => item.type === "file" && !item.uri.startsWith("data:"))

+ 115 - 74
packages/sdk/openapi.json

@@ -11371,14 +11371,7 @@
                       "$ref": "#/components/schemas/LocationInfo"
                     },
                     "data": {
-                      "anyOf": [
-                        {
-                          "$ref": "#/components/schemas/FileSystemTextContent"
-                        },
-                        {
-                          "$ref": "#/components/schemas/FileSystemBinaryContent"
-                        }
-                      ]
+                      "$ref": "#/components/schemas/FileSystemContent"
                     }
                   },
                   "required": ["location", "data"],
@@ -11507,6 +11500,112 @@
         ]
       }
     },
+    "/api/fs/find": {
+      "get": {
+        "tags": ["filesystem"],
+        "operationId": "v2.fs.find",
+        "parameters": [
+          {
+            "name": "location",
+            "in": "query",
+            "schema": {
+              "type": "object",
+              "properties": {
+                "directory": {
+                  "type": "string"
+                },
+                "workspace": {
+                  "type": "string"
+                }
+              },
+              "additionalProperties": false
+            },
+            "required": false,
+            "style": "deepObject",
+            "explode": true
+          },
+          {
+            "name": "query",
+            "in": "query",
+            "schema": {
+              "type": "string"
+            },
+            "required": true
+          },
+          {
+            "name": "type",
+            "in": "query",
+            "schema": {
+              "type": "string",
+              "enum": ["file", "directory"]
+            },
+            "required": false
+          },
+          {
+            "name": "limit",
+            "in": "query",
+            "schema": {
+              "type": "string"
+            },
+            "required": false
+          }
+        ],
+        "security": [],
+        "responses": {
+          "200": {
+            "description": "Success",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "location": {
+                      "$ref": "#/components/schemas/LocationInfo"
+                    },
+                    "data": {
+                      "type": "array",
+                      "items": {
+                        "$ref": "#/components/schemas/FileSystemEntry"
+                      }
+                    }
+                  },
+                  "required": ["location", "data"],
+                  "additionalProperties": false
+                }
+              }
+            }
+          },
+          "400": {
+            "description": "InvalidRequestError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidRequestError"
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "UnauthorizedError",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/UnauthorizedError"
+                }
+              }
+            }
+          }
+        },
+        "description": "Find recursively ranked filesystem entries relative to the requested location.",
+        "summary": "Find files",
+        "x-codeSamples": [
+          {
+            "lang": "js",
+            "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.v2.fs.find({\n  ...\n})"
+          }
+        ]
+      }
+    },
     "/api/command": {
       "get": {
         "tags": ["commands"],
@@ -21492,51 +21591,8 @@
             "type": "string",
             "enum": ["file"]
           },
-          "source": {
-            "anyOf": [
-              {
-                "type": "object",
-                "properties": {
-                  "type": {
-                    "type": "string",
-                    "enum": ["data"]
-                  },
-                  "data": {
-                    "type": "string"
-                  }
-                },
-                "required": ["type", "data"],
-                "additionalProperties": false
-              },
-              {
-                "type": "object",
-                "properties": {
-                  "type": {
-                    "type": "string",
-                    "enum": ["url"]
-                  },
-                  "url": {
-                    "type": "string"
-                  }
-                },
-                "required": ["type", "url"],
-                "additionalProperties": false
-              },
-              {
-                "type": "object",
-                "properties": {
-                  "type": {
-                    "type": "string",
-                    "enum": ["file"]
-                  },
-                  "uri": {
-                    "type": "string"
-                  }
-                },
-                "required": ["type", "uri"],
-                "additionalProperties": false
-              }
-            ]
+          "uri": {
+            "type": "string"
           },
           "mime": {
             "type": "string"
@@ -21545,7 +21601,7 @@
             "type": "string"
           }
         },
-        "required": ["type", "source", "mime"],
+        "required": ["type", "uri", "mime"],
         "additionalProperties": false
       },
       "SessionNextRetry_error": {
@@ -24986,42 +25042,27 @@
         "required": ["id", "projectID", "action", "resource"],
         "additionalProperties": false
       },
-      "FileSystemTextContent": {
+      "FileSystemContent": {
         "type": "object",
         "properties": {
-          "type": {
-            "type": "string",
-            "enum": ["text"]
-          },
-          "content": {
+          "uri": {
             "type": "string"
           },
-          "mime": {
+          "name": {
             "type": "string"
-          }
-        },
-        "required": ["type", "content", "mime"],
-        "additionalProperties": false
-      },
-      "FileSystemBinaryContent": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "enum": ["binary"]
           },
           "content": {
             "type": "string"
           },
           "encoding": {
             "type": "string",
-            "enum": ["base64"]
+            "enum": ["utf8", "base64"]
           },
           "mime": {
             "type": "string"
           }
         },
-        "required": ["type", "content", "encoding", "mime"],
+        "required": ["uri", "content", "encoding", "mime"],
         "additionalProperties": false
       },
       "FileSystemEntry": {

+ 1 - 2
packages/tui/src/feature-plugins/system/session-v2.tsx

@@ -1073,8 +1073,7 @@ function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
   return (content ?? [])
     .map((item) => {
       if (item.type === "text") return item.text.trim()
-      const source =
-        item.uri
+      const source = item.uri
       return `[file ${item.name ?? source}]`
     })
     .filter(Boolean)