diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index 7cb734ccaf..88e512abea 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -3462,29 +3462,39 @@ describe("processAgentStreamEvents", () => { expect(getAssistantTexts(result.head)).toEqual(["Identified"]); }); - it("promotes completed assistant markdown blocks to tail while keeping the live block in head", () => { - const result = processAgentStreamEvents({ + it("keeps a promoted live image block identity when the turn completes", () => { + const imageMarkdown = "![Architecture](docs/architecture.png)"; + const streaming = processAgentStreamEvents({ events: [ - makeStreamReducerEvent(makeTimelineEvent("First paragraph"), 1), - makeStreamReducerEvent(makeTimelineEvent("\n\nSecond paragraph"), 2), + makeStreamReducerEvent(makeTimelineEvent("Introductory paragraph"), 1), + makeStreamReducerEvent(makeTimelineEvent(`\n\n${imageMarkdown}`), 2), ], currentTail: [], currentHead: [], currentCursor: undefined, }); - expect(result.changedTail).toBe(true); - expect(result.changedHead).toBe(true); - expect(result.tail).toHaveLength(1); - expect(result.tail[0]).toMatchObject({ - kind: "assistant_message", - text: "First paragraph", - }); - expect(result.head).toHaveLength(1); - expect(result.head[0]).toMatchObject({ - kind: "assistant_message", - text: "Second paragraph", + expect([streaming.changedTail, streaming.changedHead]).toEqual([true, true]); + expect(getAssistantTexts(streaming.tail)).toEqual(["Introductory paragraph"]); + expect(getAssistantTexts(streaming.head)).toEqual([imageMarkdown]); + const liveImageId = streaming.head[0]?.id; + + const completed = processAgentStreamEvent({ + ...baseStreamInput, + event: { type: "turn_completed", provider: "claude" } as AgentStreamEventPayload, + currentTail: streaming.tail, + currentHead: streaming.head, + currentCursor: streaming.cursor ?? undefined, }); + const assistantBlocks = [...completed.tail, ...completed.head].filter( + (item): item is Extract => + item.kind === "assistant_message", + ); + expect(assistantBlocks.map((item) => [item.id, item.text])).toEqual([ + [streaming.tail[0]?.id, "Introductory paragraph"], + [liveImageId, imageMarkdown], + ]); + expect(assistantBlocks.filter((item) => item.id === liveImageId)).toHaveLength(1); }); it("preserves a live block trailing newline after promoting completed markdown blocks", () => { diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index e9f2ef7082..1c02d1b31c 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -1501,15 +1501,6 @@ function finalizeHeadItems(head: StreamItem[]): StreamItem[] { if (item.kind === "thought" && item.status !== "ready") { return markThoughtReady(item); } - if (item.kind === "assistant_message" && item.blockGroupId) { - return { - ...item, - id: createAssistantBlockId({ - groupId: item.blockGroupId, - blockIndex: item.blockIndex ?? 0, - }), - }; - } return item; }); } @@ -1597,7 +1588,10 @@ function promoteCompletedAssistantBlocks(params: { tail: StreamItem[]; head: Str const nextTail = flushHeadToTail(params.tail, promotedItems); const liveItem: AssistantMessageItem = { ...activeItem, - id: `${blockGroupId}:head`, + id: createAssistantBlockId({ + groupId: blockGroupId, + blockIndex: firstBlockIndex + completedBlocks.length, + }), blockGroupId, blockIndex: firstBlockIndex + completedBlocks.length, text: liveBlock,