fix(app): preserve inline file mentions (#38663)
This commit is contained in:
parent
ae4be983cb
commit
b62806683e
8 changed files with 98 additions and 22 deletions
|
|
@ -489,6 +489,9 @@ describe("prompt submit worktree selection", () => {
|
|||
agents: [],
|
||||
})
|
||||
expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_")
|
||||
expect((promptInputs[0] as { legacyParts?: { id: string; type: string; text?: string }[] }).legacyParts).toEqual([
|
||||
{ id: expect.stringMatching(/^prt_/), type: "text", text: "ls" },
|
||||
])
|
||||
})
|
||||
|
||||
test("submits slash commands through the current session API", async () => {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) {
|
|||
agent: input.draft.agent,
|
||||
model: input.draft.model,
|
||||
variant: input.draft.variant,
|
||||
legacyParts: requestParts,
|
||||
text: requestParts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"),
|
||||
files: requestParts.flatMap((part) => {
|
||||
if (part.type !== "file") return []
|
||||
|
|
|
|||
|
|
@ -69,18 +69,62 @@ describe("createCompatibleApi", () => {
|
|||
await api.session.prompt({
|
||||
sessionID: "ses_1",
|
||||
id: "msg_1",
|
||||
text: "hello",
|
||||
text: "hello @src/index.ts",
|
||||
agent: "build",
|
||||
model: { providerID: "provider", modelID: "model" },
|
||||
files: [
|
||||
{ uri: "file:///repo/src/index.ts", name: "index.ts", mention: { text: "@src/index.ts", start: 6, end: 19 } },
|
||||
{ uri: "data:text/plain;base64,aGVsbG8=", name: "notes.txt" },
|
||||
],
|
||||
})
|
||||
|
||||
expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async")
|
||||
expect(await requests[0]!.json()).toMatchObject({
|
||||
const body = await requests[0]!.json()
|
||||
expect(body).toMatchObject({
|
||||
messageID: "msg_1",
|
||||
agent: "build",
|
||||
model: { providerID: "provider", modelID: "model" },
|
||||
parts: [{ type: "text", text: "hello" }],
|
||||
parts: [
|
||||
{ type: "text", text: "hello @src/index.ts" },
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: "file:///repo/src/index.ts",
|
||||
filename: "index.ts",
|
||||
source: {
|
||||
type: "file",
|
||||
text: { value: "@src/index.ts", start: 6, end: 19 },
|
||||
path: "file:///repo/src/index.ts",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
mime: "text/plain",
|
||||
url: "data:text/plain;base64,aGVsbG8=",
|
||||
filename: "notes.txt",
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(body.parts[2]).not.toHaveProperty("source")
|
||||
})
|
||||
|
||||
test("preserves original parts for V1 optimistic reconciliation", async () => {
|
||||
const { api, requests } = setup("v1")
|
||||
await api.session.prompt({
|
||||
sessionID: "ses_1",
|
||||
id: "msg_1",
|
||||
text: "look",
|
||||
files: [{ uri: "data:image/png;base64,AAAA", name: "image.png" }],
|
||||
legacyParts: [
|
||||
{ id: "prt_text", type: "text", text: "look" },
|
||||
{ id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" },
|
||||
],
|
||||
})
|
||||
|
||||
expect((await requests[0]!.json()).parts).toEqual([
|
||||
{ id: "prt_text", type: "text", text: "look" },
|
||||
{ id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" },
|
||||
])
|
||||
})
|
||||
|
||||
test("keeps V2 session actions on the current API", async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ServerApi } from "./server"
|
||||
import type { ServerProtocol } from "./server-protocol"
|
||||
import type { OpencodeClient, Session } from "@opencode-ai/sdk/v2/client"
|
||||
import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client"
|
||||
import type {
|
||||
Project,
|
||||
ProjectCurrent,
|
||||
|
|
@ -43,6 +43,7 @@ type LegacyPrompt = {
|
|||
agent?: string
|
||||
model?: { providerID: string; modelID: string }
|
||||
variant?: string
|
||||
legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[]
|
||||
}
|
||||
type LegacyLocation = { directory?: string }
|
||||
type CompatibleInput = {
|
||||
|
|
@ -203,13 +204,20 @@ function createV1Api(input: CompatibleInput): CompatibleApi {
|
|||
agent: value.agent,
|
||||
model: value.model,
|
||||
variant: value.variant,
|
||||
parts: [
|
||||
parts: value.legacyParts ?? [
|
||||
{ type: "text", text: value.text },
|
||||
...(value.files ?? []).map((file) => ({
|
||||
type: "file" as const,
|
||||
mime: mime(file.uri),
|
||||
mime: file.mention ? "text/plain" : mime(file.uri),
|
||||
url: file.uri,
|
||||
filename: file.name,
|
||||
source: file.mention
|
||||
? {
|
||||
type: "file" as const,
|
||||
text: { value: file.mention.text, start: file.mention.start, end: file.mention.end },
|
||||
path: file.uri,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
...(value.agents ?? []).map((agent) => ({
|
||||
type: "agent" as const,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ describe("normalizeSessionMessages", () => {
|
|||
{
|
||||
id: "msg_3",
|
||||
type: "user",
|
||||
text: "inspect this",
|
||||
text: "inspect @src/client.ts",
|
||||
files: [
|
||||
{
|
||||
data: "aGVsbG8=",
|
||||
|
|
@ -23,6 +23,13 @@ describe("normalizeSessionMessages", () => {
|
|||
name: "note.txt",
|
||||
source: { type: "inline" },
|
||||
},
|
||||
{
|
||||
data: "ZXhwb3J0IHt9",
|
||||
mime: "text/plain",
|
||||
name: "client.ts",
|
||||
source: { type: "inline" },
|
||||
mention: { text: "@src/client.ts", start: 8, end: 22 },
|
||||
},
|
||||
],
|
||||
agents: [{ name: "review", mention: { text: "@review", start: 0, end: 7 } }],
|
||||
time: { created: 3 },
|
||||
|
|
@ -76,9 +83,18 @@ describe("normalizeSessionMessages", () => {
|
|||
expect(result.parts.get("msg_3")?.map((part) => part.id)).toEqual([
|
||||
"msg_3:text:0",
|
||||
"msg_3:file:0",
|
||||
"msg_3:file:1",
|
||||
"msg_3:agent:0",
|
||||
"msg_5:compaction",
|
||||
])
|
||||
expect(result.parts.get("msg_3")?.[2]).toMatchObject({
|
||||
type: "file",
|
||||
source: {
|
||||
type: "file",
|
||||
path: "src/client.ts",
|
||||
text: { value: "@src/client.ts", start: 8, end: 22 },
|
||||
},
|
||||
})
|
||||
expect(result.parts.get("msg_4")?.map((part) => part.id)).toEqual(["msg_4:reasoning:0", "msg_4:text:0", "call_1"])
|
||||
expect(result.parts.get("msg_4")?.[2]).toMatchObject({
|
||||
type: "tool",
|
||||
|
|
|
|||
|
|
@ -206,6 +206,13 @@ function userParts(sessionID: string, message: SessionMessageUser): Part[] {
|
|||
mime: file.mime,
|
||||
filename: file.name,
|
||||
url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`,
|
||||
source: file.mention
|
||||
? {
|
||||
type: "file",
|
||||
text: { value: file.mention.text, start: file.mention.start, end: file.mention.end },
|
||||
path: file.mention.text.startsWith("@") ? file.mention.text.slice(1) : (file.name ?? file.mention.text),
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
...(message.agents ?? []).map(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe("message-file", () => {
|
|||
expect(attached(file())).toBe(false)
|
||||
})
|
||||
|
||||
test("treats only non-attachment source ranges as inline references", () => {
|
||||
test("keeps data-backed file mentions inline", () => {
|
||||
expect(
|
||||
inline(
|
||||
file({
|
||||
|
|
@ -34,18 +34,16 @@ describe("message-file", () => {
|
|||
),
|
||||
).toBe(true)
|
||||
|
||||
expect(
|
||||
inline(
|
||||
file({
|
||||
url: "data:text/plain;base64,SGVsbG8=",
|
||||
source: {
|
||||
type: "file",
|
||||
path: "/repo/README.txt",
|
||||
text: { value: "@README.txt", start: 0, end: 11 },
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBe(false)
|
||||
const mentioned = file({
|
||||
url: "data:text/plain;base64,SGVsbG8=",
|
||||
source: {
|
||||
type: "file",
|
||||
path: "/repo/README.txt",
|
||||
text: { value: "@README.txt", start: 0, end: 11 },
|
||||
},
|
||||
})
|
||||
expect(inline(mentioned)).toBe(true)
|
||||
expect(attached(mentioned)).toBe(false)
|
||||
})
|
||||
|
||||
test("separates image and file attachment kinds", () => {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ import { getFilename } from "@opencode-ai/core/util/path"
|
|||
import type { FilePart } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export function attached(part: FilePart) {
|
||||
return part.url.startsWith("data:")
|
||||
return part.url.startsWith("data:") && !inline(part)
|
||||
}
|
||||
|
||||
export function inline(part: FilePart) {
|
||||
if (attached(part)) return false
|
||||
return part.source?.text?.start !== undefined && part.source?.text?.end !== undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue