feat(tui): use canonical prompt attachments
This commit is contained in:
parent
91f1815732
commit
c13f06c30c
24 changed files with 611 additions and 466 deletions
|
|
@ -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" }])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue