浏览代码

fix(core): keep resolved attachments hidden

Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Kit Langton 1 月之前
父节点
当前提交
573ce945a6

+ 60 - 33
packages/core/src/session.ts

@@ -676,16 +676,22 @@ function resolveInlineFileAttachment(file: PromptInput.FileAttachment) {
   if (mime) {
     if (textMime(mime)) {
       return {
-        file: { ...file, mime },
-        text: formatAttachmentText({ name: file.name, uri: file.uri, text: decodeDataText(file.uri) }),
+        file: {
+          ...file,
+          mime,
+          description: formatAttachmentText({ name: file.name, uri: file.uri, text: decodeDataText(file.uri) }),
+        },
       }
     }
     return { file: { ...file, mime } }
   }
   if (!URL.canParse(file.uri)) {
     return {
-      file: { ...file, mime: "application/octet-stream" },
-      text: attachmentError({ name: file.name, uri: file.uri, message: "unsupported attachment URI" }),
+      file: {
+        ...file,
+        mime: "application/octet-stream",
+        description: attachmentError({ name: file.name, uri: file.uri, message: "unsupported attachment URI" }),
+      },
     }
   }
   const url = new URL(file.uri)
@@ -696,7 +702,7 @@ function resolveInlineFileAttachment(file: PromptInput.FileAttachment) {
 function resolveInlinePrompt(input: PromptInput.Prompt) {
   const files = (input.files ?? []).map(resolveInlineFileAttachment)
   return Prompt.make({
-    text: [input.text, ...files.flatMap((file) => (file.text ? [file.text] : []))].filter(Boolean).join("\n\n"),
+    text: input.text,
     agents: input.agents,
     files: files.map((file) => file.file),
   })
@@ -707,8 +713,11 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
   if (mime) {
     if (textMime(mime)) {
       return {
-        file: { ...file, mime },
-        text: formatAttachmentText({ name: file.name, uri: file.uri, text: decodeDataText(file.uri) }),
+        file: {
+          ...file,
+          mime,
+          description: formatAttachmentText({ name: file.name, uri: file.uri, text: decodeDataText(file.uri) }),
+        },
       }
     }
     return { file: { ...file, mime } }
@@ -716,8 +725,11 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
 
   if (!URL.canParse(file.uri)) {
     return {
-      file: { ...file, mime: "application/octet-stream" },
-      text: attachmentError({ name: file.name, uri: file.uri, message: "unsupported attachment URI" }),
+      file: {
+        ...file,
+        mime: "application/octet-stream",
+        description: attachmentError({ name: file.name, uri: file.uri, message: "unsupported attachment URI" }),
+      },
     }
   }
 
@@ -733,12 +745,15 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
   const relative = path.relative(location.directory, filepath)
   if (relative.startsWith("..") || path.isAbsolute(relative)) {
     return {
-      file: { ...file, mime: FSUtil.mimeType(filepath) },
-      text: attachmentError({
-        name: file.name ?? filepath,
-        uri: file.uri,
-        message: "file is outside the session location",
-      }),
+      file: {
+        ...file,
+        mime: FSUtil.mimeType(filepath),
+        description: attachmentError({
+          name: file.name ?? filepath,
+          uri: file.uri,
+          message: "file is outside the session location",
+        }),
+      },
     }
   }
 
@@ -751,21 +766,27 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
       .map((entry) => `${entry.path}${entry.type === "directory" ? "/" : ""}`)
       .join("\n")
     return {
-      file: { ...file, mime: "application/x-directory" },
-      text: formatAttachmentText({
-        name: file.name ?? filepath,
-        uri: file.uri,
-        text: visible || "[Directory is empty]",
-        truncated: entries.length > MAX_DIRECTORY_ENTRIES,
-      }),
+      file: {
+        ...file,
+        mime: "application/x-directory",
+        description: formatAttachmentText({
+          name: file.name ?? filepath,
+          uri: file.uri,
+          text: visible || "[Directory is empty]",
+          truncated: entries.length > MAX_DIRECTORY_ENTRIES,
+        }),
+      },
     }
   }
 
   const read = yield* filesystem.read({ path: target }).pipe(Effect.exit)
   if (Exit.isFailure(read)) {
     return {
-      file: { ...file, mime: FSUtil.mimeType(filepath) },
-      text: attachmentError({ name: file.name ?? filepath, uri: file.uri, message: "file not found" }),
+      file: {
+        ...file,
+        mime: FSUtil.mimeType(filepath),
+        description: attachmentError({ name: file.name ?? filepath, uri: file.uri, message: "file not found" }),
+      },
     }
   }
   const content = read.value
@@ -774,8 +795,11 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
     const selected = selectedLines(Buffer.from(content.content).toString("utf8"), url)
     const truncated = truncateAttachmentText(selected.text, selected.truncated)
     return {
-      file: { ...file, mime: "text/plain" },
-      text: formatAttachmentText({ name: file.name ?? filepath, uri: file.uri, ...truncated }),
+      file: {
+        ...file,
+        mime: "text/plain",
+        description: formatAttachmentText({ name: file.name ?? filepath, uri: file.uri, ...truncated }),
+      },
     }
   }
 
@@ -790,19 +814,22 @@ const resolveFileAttachment = Effect.fn("Session.resolveFileAttachment")(functio
   }
 
   return {
-    file: { ...file, mime: content.mime },
-    text: attachmentError({
-      name: file.name ?? filepath,
-      uri: file.uri,
-      message: `unsupported file type ${content.mime}`,
-    }),
+    file: {
+      ...file,
+      mime: content.mime,
+      description: attachmentError({
+        name: file.name ?? filepath,
+        uri: file.uri,
+        message: `unsupported file type ${content.mime}`,
+      }),
+    },
   }
 })
 
 const resolvePrompt = Effect.fn("Session.resolvePrompt")(function* (input: PromptInput.Prompt) {
   const files = yield* Effect.forEach(input.files ?? [], resolveFileAttachment, { concurrency: "unbounded" })
   return Prompt.make({
-    text: [input.text, ...files.flatMap((file) => (file.text ? [file.text] : []))].filter(Boolean).join("\n\n"),
+    text: input.text,
     agents: input.agents,
     files: files.map((file) => file.file),
   })

+ 3 - 2
packages/core/src/session/runner/to-llm-message.ts

@@ -11,7 +11,9 @@ import { SessionMessage } from "../message"
 import type { FileAttachment } from "../prompt"
 
 const media = (file: FileAttachment): ContentPart[] => {
-  if (file.mime === "text/plain" || file.mime === "application/x-directory") return []
+  if (file.mime === "text/plain" || file.mime === "application/x-directory") {
+    return file.description ? [{ type: "text", text: file.description }] : []
+  }
   if (!file.mime.startsWith("image/") && file.mime !== "application/pdf") return []
   return [
     {
@@ -19,7 +21,6 @@ const media = (file: FileAttachment): ContentPart[] => {
       mediaType: file.mime,
       data: file.uri,
       filename: file.name,
-      metadata: file.description === undefined ? undefined : { description: file.description },
     },
   ]
 }

+ 7 - 6
packages/core/test/session-prompt.test.ts

@@ -248,9 +248,9 @@ describe("SessionV2.prompt", () => {
         resume: false,
       })
 
-      expect(message.prompt.text).toContain("explain this")
-      expect(message.prompt.text).toContain('<attachment name="src/example.ts#2-3">')
-      expect(message.prompt.text).toContain("two\nthree")
+      expect(message.prompt.text).toBe("explain this")
+      expect(message.prompt.files?.[0]?.description).toContain('<attachment name="src/example.ts#2-3">')
+      expect(message.prompt.files?.[0]?.description).toContain("two\nthree")
       expect(message.prompt.files?.[0]?.mime).toBe("text/plain")
       expect(message.prompt.files?.[0]?.source?.text).toBe("@src/example.ts#2-3")
     }),
@@ -274,9 +274,10 @@ describe("SessionV2.prompt", () => {
         resume: false,
       })
 
-      expect(message.prompt.text).toContain('<attachment name="src/">')
-      expect(message.prompt.text).toContain("index.ts")
-      expect(message.prompt.text).toContain("nested/")
+      expect(message.prompt.text).toBe("inspect directory")
+      expect(message.prompt.files?.[0]?.description).toContain('<attachment name="src/">')
+      expect(message.prompt.files?.[0]?.description).toContain("index.ts")
+      expect(message.prompt.files?.[0]?.description).toContain("nested/")
       expect(message.prompt.files?.[0]?.mime).toBe("application/x-directory")
     }),
   )