diff --git a/packages/app/src/timeline/session-stream-reducers.test.ts b/packages/app/src/timeline/session-stream-reducers.test.ts index abd7455aa..556b41bca 100644 --- a/packages/app/src/timeline/session-stream-reducers.test.ts +++ b/packages/app/src/timeline/session-stream-reducers.test.ts @@ -955,6 +955,38 @@ describe("processAgentStreamEvents", () => { }); }); + it("preserves a live block trailing newline after promoting completed markdown blocks", () => { + const result = processAgentStreamEvents({ + events: [ + makeStreamReducerEvent( + makeTimelineEvent( + "Done. I added `[TimelineMerge] ...` logging around the suspicious merge", + ), + 238, + ), + makeStreamReducerEvent(makeTimelineEvent("/reconcile paths.\n\nChanged:\n"), 239), + makeStreamReducerEvent(makeTimelineEvent("- [timeline-debug.ts]"), 240), + ], + currentTail: [], + currentHead: [], + currentCursor: undefined, + currentAgent: null, + }); + + expect(result.changedTail).toBe(true); + expect(result.changedHead).toBe(true); + expect(result.tail).toHaveLength(1); + expect(result.tail[0]).toMatchObject({ + kind: "assistant_message", + text: "Done. I added `[TimelineMerge] ...` logging around the suspicious merge/reconcile paths.", + }); + expect(result.head).toHaveLength(1); + expect(result.head[0]).toMatchObject({ + kind: "assistant_message", + text: "Changed:\n- [timeline-debug.ts]", + }); + }); + it("does not promote a markdown block that is still inside an open code fence", () => { const result = processAgentStreamEvents({ events: [ diff --git a/packages/app/src/types/stream.ts b/packages/app/src/types/stream.ts index 350cab6d4..9f8570731 100644 --- a/packages/app/src/types/stream.ts +++ b/packages/app/src/types/stream.ts @@ -793,6 +793,10 @@ function createAssistantBlockId(params: { groupId: string; blockIndex: number }) return `${params.groupId}:block:${params.blockIndex}`; } +function getTrailingNewlineSuffix(text: string): string { + return /\n+$/.exec(text)?.[0] ?? ""; +} + function getActiveAssistantHeadIndex(head: StreamItem[]): number { for (let index = head.length - 1; index >= 0; index -= 1) { if (head[index]?.kind === "assistant_message") { @@ -832,7 +836,7 @@ function promoteCompletedAssistantBlocks(params: { tail: StreamItem[]; head: Str const blockGroupId = activeItem.blockGroupId ?? activeItem.id; const firstBlockIndex = activeItem.blockIndex ?? 0; const completedBlocks = blocks.slice(0, -1); - const liveBlock = blocks[blocks.length - 1] ?? ""; + const liveBlock = `${blocks[blocks.length - 1] ?? ""}${getTrailingNewlineSuffix(activeItem.text)}`; const promotedItems = completedBlocks.map((block, offset) => ({ kind: "assistant_message", id: createAssistantBlockId({