From 573ce945a6fee1591120325aa0aa1ea65a935974 Mon Sep 17 00:00:00 2001 From: Kit Langton <7587245+kitlangton@users.noreply.github.com> Date: Tue, 30 Jun 2026 14:44:46 +0000 Subject: [PATCH] fix(core): keep resolved attachments hidden Co-authored-by: opencode-agent[bot] --- packages/core/src/session.ts | 93 ++++++++++++------- .../core/src/session/runner/to-llm-message.ts | 5 +- packages/core/test/session-prompt.test.ts | 13 +-- 3 files changed, 70 insertions(+), 41 deletions(-) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index f625cf3c5d..a6be47d1e0 100644 --- a/packages/core/src/session.ts +++ b/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), }) diff --git a/packages/core/src/session/runner/to-llm-message.ts b/packages/core/src/session/runner/to-llm-message.ts index a15da4a723..1aac7070f4 100644 --- a/packages/core/src/session/runner/to-llm-message.ts +++ b/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 }, }, ] } diff --git a/packages/core/test/session-prompt.test.ts b/packages/core/test/session-prompt.test.ts index ed3f7e5207..4b6059cd72 100644 --- a/packages/core/test/session-prompt.test.ts +++ b/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('') - expect(message.prompt.text).toContain("two\nthree") + expect(message.prompt.text).toBe("explain this") + expect(message.prompt.files?.[0]?.description).toContain('') + 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('') - 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('') + 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") }), )