diff --git a/packages/app/src/types/stream.test.ts b/packages/app/src/types/stream.test.ts index 20d953a5d..6d3cc93e2 100644 --- a/packages/app/src/types/stream.test.ts +++ b/packages/app/src/types/stream.test.ts @@ -1457,6 +1457,52 @@ describe("turn lifecycle events", () => { ); }); + it("does not shift later prompts when an earlier optimistic prompt has no canonical echo", () => { + const staleTimestamp = new Date("2025-01-01T15:04:00Z"); + const submittedTimestamp = new Date("2025-01-01T15:04:01Z"); + const stalePrompt: StreamItem = { + kind: "user_message", + id: "msg_stale", + text: "first prompt without an echo", + timestamp: staleTimestamp, + optimistic: true, + }; + const submittedPrompt: StreamItem = { + kind: "user_message", + id: "msg_submitted", + text: "later submitted prompt", + timestamp: submittedTimestamp, + optimistic: true, + }; + + const state = reduceStreamUpdate( + [stalePrompt, submittedPrompt], + { + type: "timeline", + provider: "codex", + item: { + type: "user_message", + text: "canonical rendered prompt", + messageId: "provider-owned-submitted", + clientMessageId: submittedPrompt.id, + }, + }, + new Date("2025-01-01T15:04:02Z"), + { source: "live" }, + ); + + assert.deepStrictEqual(state, [ + stalePrompt, + { + kind: "user_message", + id: "provider-owned-submitted", + clientMessageId: submittedPrompt.id, + text: submittedPrompt.text, + timestamp: submittedPrompt.timestamp, + }, + ]); + }); + it("appends a live server user message when no optimistic user message is pending", () => { const state = reduceStreamUpdate( [], diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 21d2726d3..edd1ab36d 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -365,7 +365,10 @@ function appendUserMessage( const chunkSeed = chunk.trim() || chunk; const entryId = messageId ?? createUniqueTimelineId(state, "user", chunkSeed, timestamp); const optimisticIndex = state.findIndex( - (entry) => entry.kind === "user_message" && entry.optimistic, + (entry) => + entry.kind === "user_message" && + entry.optimistic && + (clientMessageId === undefined || entry.id === clientMessageId), ); const optimistic = optimisticIndex >= 0 ? (state[optimisticIndex] as UserMessageItem) : null;