fix forked prompt attachments losing file parts (#17815)
This commit is contained in:
@@ -13,6 +13,7 @@ import { MessageID, PartID } from "@/session/schema"
|
|||||||
import { createStore, produce } from "solid-js/store"
|
import { createStore, produce } from "solid-js/store"
|
||||||
import { useKeybind } from "@tui/context/keybind"
|
import { useKeybind } from "@tui/context/keybind"
|
||||||
import { usePromptHistory, type PromptInfo } from "./history"
|
import { usePromptHistory, type PromptInfo } from "./history"
|
||||||
|
import { assign } from "./part"
|
||||||
import { usePromptStash } from "./stash"
|
import { usePromptStash } from "./stash"
|
||||||
import { DialogStash } from "../dialog-stash"
|
import { DialogStash } from "../dialog-stash"
|
||||||
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
import { type AutocompleteRef, Autocomplete } from "./autocomplete"
|
||||||
@@ -643,10 +644,7 @@ export function Prompt(props: PromptProps) {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: inputText,
|
text: inputText,
|
||||||
},
|
},
|
||||||
...nonTextParts.map((x) => ({
|
...nonTextParts.map(assign),
|
||||||
id: PartID.ascending(),
|
|
||||||
...x,
|
|
||||||
})),
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { PartID } from "@/session/schema"
|
||||||
|
import type { PromptInfo } from "./history"
|
||||||
|
|
||||||
|
type Item = PromptInfo["parts"][number]
|
||||||
|
|
||||||
|
export function strip(part: Item & { id: string; messageID: string; sessionID: string }): Item {
|
||||||
|
const { id: _id, messageID: _messageID, sessionID: _sessionID, ...rest } = part
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assign(part: Item): Item & { id: PartID } {
|
||||||
|
return {
|
||||||
|
...part,
|
||||||
|
id: PartID.ascending(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { useSDK } from "@tui/context/sdk"
|
|||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { useDialog } from "../../ui/dialog"
|
import { useDialog } from "../../ui/dialog"
|
||||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||||
|
import { strip } from "@tui/component/prompt/part"
|
||||||
|
|
||||||
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
export function DialogForkFromTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
||||||
const sync = useSync()
|
const sync = useSync()
|
||||||
@@ -42,7 +43,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
|
|||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
if (!part.synthetic) agg.input += part.text
|
if (!part.synthetic) agg.input += part.text
|
||||||
}
|
}
|
||||||
if (part.type === "file") agg.parts.push(part)
|
if (part.type === "file") agg.parts.push(strip(part))
|
||||||
return agg
|
return agg
|
||||||
},
|
},
|
||||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useSDK } from "@tui/context/sdk"
|
|||||||
import { useRoute } from "@tui/context/route"
|
import { useRoute } from "@tui/context/route"
|
||||||
import { Clipboard } from "@tui/util/clipboard"
|
import { Clipboard } from "@tui/util/clipboard"
|
||||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||||
|
import { strip } from "@tui/component/prompt/part"
|
||||||
|
|
||||||
export function DialogMessage(props: {
|
export function DialogMessage(props: {
|
||||||
messageID: string
|
messageID: string
|
||||||
@@ -40,7 +41,7 @@ export function DialogMessage(props: {
|
|||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
if (!part.synthetic) agg.input += part.text
|
if (!part.synthetic) agg.input += part.text
|
||||||
}
|
}
|
||||||
if (part.type === "file") agg.parts.push(part)
|
if (part.type === "file") agg.parts.push(strip(part))
|
||||||
return agg
|
return agg
|
||||||
},
|
},
|
||||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import type { PromptInfo } from "../../../../src/cli/cmd/tui/component/prompt/history"
|
||||||
|
import { assign, strip } from "../../../../src/cli/cmd/tui/component/prompt/part"
|
||||||
|
|
||||||
|
describe("prompt part", () => {
|
||||||
|
test("strip removes persisted ids from reused file parts", () => {
|
||||||
|
const part = {
|
||||||
|
id: "prt_old",
|
||||||
|
sessionID: "ses_old",
|
||||||
|
messageID: "msg_old",
|
||||||
|
type: "file" as const,
|
||||||
|
mime: "image/png",
|
||||||
|
filename: "tiny.png",
|
||||||
|
url: "data:image/png;base64,abc",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(strip(part)).toEqual({
|
||||||
|
type: "file",
|
||||||
|
mime: "image/png",
|
||||||
|
filename: "tiny.png",
|
||||||
|
url: "data:image/png;base64,abc",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("assign overwrites stale runtime ids", () => {
|
||||||
|
const part = {
|
||||||
|
id: "prt_old",
|
||||||
|
sessionID: "ses_old",
|
||||||
|
messageID: "msg_old",
|
||||||
|
type: "file" as const,
|
||||||
|
mime: "image/png",
|
||||||
|
filename: "tiny.png",
|
||||||
|
url: "data:image/png;base64,abc",
|
||||||
|
} as PromptInfo["parts"][number]
|
||||||
|
|
||||||
|
const next = assign(part)
|
||||||
|
|
||||||
|
expect(next.id).not.toBe("prt_old")
|
||||||
|
expect(next.id.startsWith("prt_")).toBe(true)
|
||||||
|
expect(next).toMatchObject({
|
||||||
|
type: "file",
|
||||||
|
mime: "image/png",
|
||||||
|
filename: "tiny.png",
|
||||||
|
url: "data:image/png;base64,abc",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user