Skip to content

Commit 0d7e62a

Browse files
authored
fix forked prompt attachments losing file parts (#17815)
1 parent 41aa254 commit 0d7e62a

File tree

5 files changed

+69
-6
lines changed

5 files changed

+69
-6
lines changed

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { MessageID, PartID } from "@/session/schema"
1313
import { createStore, produce } from "solid-js/store"
1414
import { useKeybind } from "@tui/context/keybind"
1515
import { usePromptHistory, type PromptInfo } from "./history"
16+
import { assign } from "./part"
1617
import { usePromptStash } from "./stash"
1718
import { DialogStash } from "../dialog-stash"
1819
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
@@ -643,10 +644,7 @@ export function Prompt(props: PromptProps) {
643644
type: "text",
644645
text: inputText,
645646
},
646-
...nonTextParts.map((x) => ({
647-
id: PartID.ascending(),
648-
...x,
649-
})),
647+
...nonTextParts.map(assign),
650648
],
651649
})
652650
.catch(() => {})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { PartID } from "@/session/schema"
2+
import type { PromptInfo } from "./history"
3+
4+
type Item = PromptInfo["parts"][number]
5+
6+
export function strip(part: Item & { id: string; messageID: string; sessionID: string }): Item {
7+
const { id: _id, messageID: _messageID, sessionID: _sessionID, ...rest } = part
8+
return rest
9+
}
10+
11+
export function assign(part: Item): Item & { id: PartID } {
12+
return {
13+
...part,
14+
id: PartID.ascending(),
15+
}
16+
}

packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useSDK } from "@tui/context/sdk"
77
import { useRoute } from "@tui/context/route"
88
import { useDialog } from "../../ui/dialog"
99
import type { PromptInfo } from "@tui/component/prompt/history"
10+
import { strip } from "@tui/component/prompt/part"
1011

1112
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
1213
const sync = useSync()
@@ -42,7 +43,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
4243
if (part.type === "text") {
4344
if (!part.synthetic) agg.input += part.text
4445
}
45-
if (part.type === "file") agg.parts.push(part)
46+
if (part.type === "file") agg.parts.push(strip(part))
4647
return agg
4748
},
4849
{ input: "", parts: [] as PromptInfo["parts"] },

packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSDK } from "@tui/context/sdk"
55
import { useRoute } from "@tui/context/route"
66
import { Clipboard } from "@tui/util/clipboard"
77
import type { PromptInfo } from "@tui/component/prompt/history"
8+
import { strip } from "@tui/component/prompt/part"
89

910
export function DialogMessage(props: {
1011
messageID: string
@@ -40,7 +41,7 @@ export function DialogMessage(props: {
4041
if (part.type === "text") {
4142
if (!part.synthetic) agg.input += part.text
4243
}
43-
if (part.type === "file") agg.parts.push(part)
44+
if (part.type === "file") agg.parts.push(strip(part))
4445
return agg
4546
},
4647
{ input: "", parts: [] as PromptInfo["parts"] },
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, test } from "bun:test"
2+
import type { PromptInfo } from "../../../../src/cli/cmd/tui/component/prompt/history"
3+
import { assign, strip } from "../../../../src/cli/cmd/tui/component/prompt/part"
4+
5+
describe("prompt part", () => {
6+
test("strip removes persisted ids from reused file parts", () => {
7+
const part = {
8+
id: "prt_old",
9+
sessionID: "ses_old",
10+
messageID: "msg_old",
11+
type: "file" as const,
12+
mime: "image/png",
13+
filename: "tiny.png",
14+
url: "data:image/png;base64,abc",
15+
}
16+
17+
expect(strip(part)).toEqual({
18+
type: "file",
19+
mime: "image/png",
20+
filename: "tiny.png",
21+
url: "data:image/png;base64,abc",
22+
})
23+
})
24+
25+
test("assign overwrites stale runtime ids", () => {
26+
const part = {
27+
id: "prt_old",
28+
sessionID: "ses_old",
29+
messageID: "msg_old",
30+
type: "file" as const,
31+
mime: "image/png",
32+
filename: "tiny.png",
33+
url: "data:image/png;base64,abc",
34+
} as PromptInfo["parts"][number]
35+
36+
const next = assign(part)
37+
38+
expect(next.id).not.toBe("prt_old")
39+
expect(next.id.startsWith("prt_")).toBe(true)
40+
expect(next).toMatchObject({
41+
type: "file",
42+
mime: "image/png",
43+
filename: "tiny.png",
44+
url: "data:image/png;base64,abc",
45+
})
46+
})
47+
})

0 commit comments

Comments
 (0)