feat(core): resolve prompt attachments

This commit is contained in:
Dax Raad 2026-07-06 15:04:23 -04:00
commit 4d100de194
29 changed files with 610 additions and 426 deletions

View file

@ -106,7 +106,11 @@ export const serializeToolContent = (content: SessionMessage.ToolStateCompleted[
const serialize = (message: SessionMessage.Message) => {
if (message.type === "user") {
const files = message.files?.map((file) => `[Attached ${file.mime}: ${file.name ?? file.uri}]`) ?? []
const files =
message.files?.map(
(file) =>
`[Attached ${file.mime}: ${file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment")}]`,
) ?? []
return [`[User]: ${message.text}`, ...files].join("\n")
}
if (message.type === "assistant") {

View file

@ -7,7 +7,7 @@ import type { Database } from "../database/database"
import type { EventV2 } from "../event"
import { SessionEvent } from "./event"
import { SessionMessage } from "./message"
import { Prompt } from "./prompt"
import { Prompt } from "@opencode-ai/schema/prompt"
import { SessionSchema } from "./schema"
import { SessionInputTable, SessionMessageTable } from "./sql"

View file

@ -1 +0,0 @@
export { AgentAttachment, FileAttachment, Prompt, Source } from "@opencode-ai/schema/prompt"

View file

@ -9,12 +9,14 @@ import {
} from "@opencode-ai/llm"
import { Option, Schema } from "effect"
import { SessionMessage } from "../message"
import type { FileAttachment } from "../prompt"
import type { FileAttachment } from "@opencode-ai/schema/prompt"
const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"])
const media = (file: FileAttachment): ContentPart => ({
type: "media",
mediaType: file.mime,
data: file.uri,
data: file.data,
filename: file.name,
metadata: file.description === undefined ? undefined : { description: file.description },
})
@ -23,37 +25,22 @@ const textAttachment = (file: FileAttachment) =>
Message.make({
role: "user",
content: [
`Attached file: ${file.name ?? file.uri}`,
`Source: ${file.uri}`,
`Attached file: ${file.name ?? (file.source.type === "uri" ? file.source.uri : "inline attachment")}`,
file.description === undefined ? undefined : `Description: ${file.description}`,
"",
file.content ?? readTextData(file.uri) ?? "[Attachment content unavailable]",
Buffer.from(file.data, "base64").toString("utf8"),
]
.filter((line): line is string => line !== undefined)
.join("\n"),
metadata: {
attachment: {
uri: file.uri,
source: file.source,
name: file.name,
description: file.description,
},
},
})
function readTextData(uri: string) {
if (!uri.startsWith("data:")) return
const comma = uri.indexOf(",")
if (comma === -1) return
const metadata = uri.slice(5, comma)
const payload = uri.slice(comma + 1)
if (metadata.split(";").some((part) => part.toLowerCase() === "base64")) return Buffer.from(payload, "base64").toString("utf8")
try {
return decodeURIComponent(payload)
} catch {
return
}
}
const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const toolInput = (tool: SessionMessage.AssistantTool) =>
@ -160,7 +147,10 @@ function toLLMMessage(message: SessionMessage.Message, model: Model): Message[]
Message.make({
id: message.id,
role: "user",
content: [{ type: "text", text: message.text }, ...files.filter((file) => file.mime !== "text/plain").map(media)],
content: [
{ type: "text", text: message.text },
...files.filter((file) => imageMimes.has(file.mime)).map(media),
],
metadata: {
...message.metadata,
...(message.agents?.length ? { agents: message.agents } : {}),

View file

@ -2,7 +2,7 @@ import { sqliteTable, text, integer, index, primaryKey, real, uniqueIndex } from
import { directoryColumn, pathColumn } from "../database/path"
import { ProjectTable } from "../project/sql"
import type { SessionMessage } from "./message"
import type { Prompt } from "./prompt"
import type { Prompt } from "@opencode-ai/schema/prompt"
import type { SessionInput } from "./input"
import type { Snapshot } from "../snapshot"
import { PermissionV1 } from "../v1/permission"