feat(tui): use canonical prompt attachments

This commit is contained in:
Dax Raad 2026-07-06 14:34:57 -04:00
commit c13f06c30c
24 changed files with 611 additions and 466 deletions

View file

@ -1,5 +1,7 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
import path from "path"
import { pathToFileURL } from "url"
import { eq } from "drizzle-orm"
import { Database } from "@opencode-ai/core/database/database"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
@ -221,20 +223,70 @@ describe("SessionV2.prompt", () => {
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const uri =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
const message = yield* session.prompt({
sessionID,
prompt: {
text: "Inspect this image",
files: [{ uri: "data:image/png;base64,aGVsbG8=", name: "image.png" }],
files: [{ uri, name: "image.png" }],
},
resume: false,
})
expect(message.prompt.files).toEqual([{ uri, name: "image.png", mime: "image/png" }])
expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files)
}),
)
it.effect("classifies source files and directories from local content", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const directory = import.meta.dir
const source = path.join(directory, "session-prompt.test.ts")
const sourceUri = pathToFileURL(source)
sourceUri.searchParams.set("start", "1")
sourceUri.searchParams.set("end", "1")
const message = yield* session.prompt({
sessionID,
prompt: {
text: "Inspect these",
files: [
{ uri: sourceUri.href, name: "main.ts" },
{ uri: pathToFileURL(directory).href, name: "source" },
],
},
resume: false,
})
expect(message.prompt.files).toEqual([
{ uri: "data:image/png;base64,aGVsbG8=", name: "image.png", mime: "image/png" },
{
uri: sourceUri.href,
name: "main.ts",
mime: "text/plain",
content: 'import { describe, expect } from "bun:test"',
},
{ uri: pathToFileURL(directory).href, name: "source", mime: "application/x-directory" },
])
expect((yield* admitted(message.id))?.prompt.files).toEqual(message.prompt.files)
}),
)
it.effect("sniffs data URL content instead of trusting its declared MIME", () =>
Effect.gen(function* () {
yield* setup
const session = yield* SessionV2.Service
const uri = `data:video/mp2t;base64,${Buffer.from("export const value = 1\n").toString("base64")}`
const message = yield* session.prompt({
sessionID,
prompt: { text: "Inspect this", files: [{ uri, name: "main.ts" }] },
resume: false,
})
expect(message.prompt.files).toEqual([{ uri, name: "main.ts", mime: "text/plain" }])
}),
)

View file

@ -149,6 +149,67 @@ Recent work
])
})
test("lowers text attachments as separate user messages", () => {
const file = FileAttachment.make({
uri: "file:///project/main.ts",
mime: "text/plain",
content: "export const value = 1",
name: "main.ts",
})
const messages = toLLMMessages(
[
SessionMessage.User.make({
id: id("user-text-file"),
type: "user",
text: "Review this file",
files: [file],
time: { created },
}),
],
model,
)
expect(messages).toHaveLength(2)
expect(messages[0]).toMatchObject({
role: "user",
content: [
{
type: "text",
text: "Attached file: main.ts\nSource: file:///project/main.ts\n\nexport const value = 1",
},
],
metadata: { attachment: { uri: "file:///project/main.ts", name: "main.ts" } },
})
expect(messages[1]).toMatchObject({
id: id("user-text-file"),
role: "user",
content: [{ type: "text", text: "Review this file" }],
})
})
test("derives text attachment content from durable data URLs", () => {
const uri = `data:text/plain;base64,${Buffer.from("inline content").toString("base64")}`
const messages = toLLMMessages(
[
SessionMessage.User.make({
id: id("user-data-file"),
type: "user",
text: "Review this file",
files: [FileAttachment.make({ uri, mime: "text/plain", name: "inline.txt" })],
time: { created },
}),
],
model,
)
expect(messages[0]?.content).toEqual([
{
type: "text",
text: `Attached file: inline.txt\nSource: ${uri}\n\ninline content`,
},
])
})
test("replays durable tool media into canonical tool messages without structured base64", () => {
const messages = toLLMMessages(
[