fix(sync): replace every optimistic part of a just-sent message
The server echoes a sent message part by part. The reducer replaced an optimistic part with its server echo only while the FIRST part of the message was still optimistic, so once the text echo had landed the file echo no longer qualified and was appended instead: an attached image showed twice until the next page fetch rewrote the parts. Any remaining optimistic part of the same type is now a replacement candidate. The scan only runs when a part with a new id arrives, which is once per part during assistant streaming. Testing: reducer test extended past the first echo to the file echo; ui type-check and lint; verified the event order against a live OpenCode (message, text part, file part within ~70ms of the accepted request).
This commit is contained in:
@@ -128,6 +128,15 @@ describe("applyDirectoryEvent", () => {
|
|||||||
properties: { part: serverText },
|
properties: { part: serverText },
|
||||||
} as Event)).toBe(true)
|
} as Event)).toBe(true)
|
||||||
expect(draft.part.msg_1).toEqual([serverText, optimisticFile])
|
expect(draft.part.msg_1).toEqual([serverText, optimisticFile])
|
||||||
|
|
||||||
|
// The file echo follows the text echo; it must claim the optimistic file
|
||||||
|
// even though the first slot now holds a server part.
|
||||||
|
const serverFile = { id: "prt_server_file", messageID: "msg_1", sessionID: "ses_1", type: "file", filename: "a.png" } as Part
|
||||||
|
expect(applyDirectoryEvent(draft, {
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: { part: serverFile },
|
||||||
|
} as Event)).toBe(true)
|
||||||
|
expect(draft.part.msg_1).toEqual([serverText, serverFile])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns typed materialization when delta arrives before parts", () => {
|
test("returns typed materialization when delta arrives before parts", () => {
|
||||||
|
|||||||
@@ -433,11 +433,13 @@ export function applyDirectoryEvent(
|
|||||||
: part
|
: part
|
||||||
} else {
|
} else {
|
||||||
// Replace optimistic part (no sessionID) with server part of same type.
|
// Replace optimistic part (no sessionID) with server part of same type.
|
||||||
// Gate: only scan if the first part lacks sessionID (optimistic parts are
|
// Every optimistic part is a candidate, not only the first one: the
|
||||||
// always inserted first). Assistant messages never have optimistic parts,
|
// server echoes a just-sent message part by part, and after the text
|
||||||
// so this check is effectively free during streaming.
|
// echo replaced the first slot the file echo still has to find the
|
||||||
const hasOptimistic = next.length > 0 && !(next[0] as { sessionID?: string }).sessionID
|
// optimistic file behind it, or the attachment shows twice until the
|
||||||
const optimisticIndex = hasOptimistic && (part.type === "text" || part.type === "file")
|
// next page fetch. The scan runs only when a part with a NEW id
|
||||||
|
// arrives, which during assistant streaming is once per part.
|
||||||
|
const optimisticIndex = part.type === "text" || part.type === "file"
|
||||||
? next.findIndex((p) => p.type === part.type && !(p as { sessionID?: string }).sessionID)
|
? next.findIndex((p) => p.type === part.type && !(p as { sessionID?: string }).sessionID)
|
||||||
: -1
|
: -1
|
||||||
if (optimisticIndex >= 0) {
|
if (optimisticIndex >= 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user