Browse Source

fix forked prompt attachments losing file parts (#17815)

Kit Langton 5 months ago
parent
commit
0d7e62a532

+ 2 - 4
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -13,6 +13,7 @@ import { MessageID, PartID } from "@/session/schema"
 import { createStore, produce } from "solid-js/store"
 import { useKeybind } from "@tui/context/keybind"
 import { usePromptHistory, type PromptInfo } from "./history"
+import { assign } from "./part"
 import { usePromptStash } from "./stash"
 import { DialogStash } from "../dialog-stash"
 import { type AutocompleteRef, Autocomplete } from "./autocomplete"
@@ -643,10 +644,7 @@ export function Prompt(props: PromptProps) {
               type: "text",
               text: inputText,
             },
-            ...nonTextParts.map((x) => ({
-              id: PartID.ascending(),
-              ...x,
-            })),
+            ...nonTextParts.map(assign),
           ],
         })
         .catch(() => {})

+ 16 - 0
packages/opencode/src/cli/cmd/tui/component/prompt/part.ts

@@ -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(),
+  }
+}

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

@@ -7,6 +7,7 @@ import { useSDK } from "@tui/context/sdk"
 import { useRoute } from "@tui/context/route"
 import { useDialog } from "../../ui/dialog"
 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 }) {
   const sync = useSync()
@@ -42,7 +43,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
               if (part.type === "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
             },
             { input: "", parts: [] as PromptInfo["parts"] },

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

@@ -5,6 +5,7 @@ import { useSDK } from "@tui/context/sdk"
 import { useRoute } from "@tui/context/route"
 import { Clipboard } from "@tui/util/clipboard"
 import type { PromptInfo } from "@tui/component/prompt/history"
+import { strip } from "@tui/component/prompt/part"
 
 export function DialogMessage(props: {
   messageID: string
@@ -40,7 +41,7 @@ export function DialogMessage(props: {
                   if (part.type === "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
                 },
                 { input: "", parts: [] as PromptInfo["parts"] },

+ 47 - 0
packages/opencode/test/cli/cmd/tui/prompt-part.test.ts

@@ -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",
+    })
+  })
+})