Przeglądaj źródła

fix(task): prevent duplicate streamed text rows after completion (#9235)

* fix(task): prevent duplicate partial text rows after completion

Avoid adding a new partial text message when the latest text row is already completed with the same content. This stops a presenter race from rendering duplicate streamed text lines for MiniMax-style timing.

Co-authored-by: Cursor <[email protected]>

* test(task): cover duplicate partial text dedupe behavior

Add a Task.say unit test that reproduces the duplicate-partial-after-complete scenario and verifies we skip creating a second text row with identical content.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Robin Newhouse 2 miesięcy temu
rodzic
commit
b514f18

+ 5 - 0
.changeset/long-poems-double.md

@@ -0,0 +1,5 @@
+---
+"cline": patch
+---
+
+Prevent duplicate streamed text rows when a partial text update arrives after the same text was already finalized.

+ 90 - 0
src/core/task/__tests__/Task.say.test.ts

@@ -0,0 +1,90 @@
+import { Task } from "@core/task"
+import { expect } from "chai"
+import sinon from "sinon"
+
+describe("Task.say", () => {
+	it("skips duplicate partial text when the same completed text was just rendered", async () => {
+		const addToClineMessages = sinon.stub().resolves()
+		const updateClineMessage = sinon.stub().resolves()
+		const postStateToWebview = sinon.stub().resolves()
+
+		const fakeTask: any = {
+			taskState: { abort: false, lastMessageTs: 0 },
+			getCurrentProviderInfo: () => ({
+				providerId: "minimax",
+				model: { id: "MiniMax-M2.1" },
+				mode: "act",
+			}),
+			messageStateHandler: {
+				getClineMessages: () => [
+					{
+						type: "say",
+						say: "text",
+						text: "Hello! How can I help you today?",
+						partial: false,
+						ts: Date.now(),
+					},
+				],
+				addToClineMessages,
+				updateClineMessage,
+			},
+			postStateToWebview,
+		}
+
+		const result = await Task.prototype.say.call(
+			fakeTask,
+			"text",
+			"Hello! How can I help you today?",
+			undefined,
+			undefined,
+			true,
+		)
+
+		expect(result).to.equal(undefined)
+		expect(addToClineMessages.called).to.equal(false)
+		expect(updateClineMessage.called).to.equal(false)
+		expect(postStateToWebview.called).to.equal(false)
+	})
+
+	it("still adds a partial text when content differs", async () => {
+		const addToClineMessages = sinon.stub().resolves()
+		const updateClineMessage = sinon.stub().resolves()
+		const postStateToWebview = sinon.stub().resolves()
+
+		const fakeTask: any = {
+			taskState: { abort: false, lastMessageTs: 0 },
+			getCurrentProviderInfo: () => ({
+				providerId: "minimax",
+				model: { id: "MiniMax-M2.1" },
+				mode: "act",
+			}),
+			messageStateHandler: {
+				getClineMessages: () => [
+					{
+						type: "say",
+						say: "text",
+						text: "Hello! How can I help you today?",
+						partial: false,
+						ts: Date.now(),
+					},
+				],
+				addToClineMessages,
+				updateClineMessage,
+			},
+			postStateToWebview,
+		}
+
+		await Task.prototype.say.call(
+			fakeTask,
+			"text",
+			"Hello! How can I help you today? More details...",
+			undefined,
+			undefined,
+			true,
+		)
+
+		expect(addToClineMessages.calledOnce).to.equal(true)
+		expect(updateClineMessage.called).to.equal(false)
+		expect(postStateToWebview.calledOnce).to.equal(true)
+	})
+})

+ 11 - 0
src/core/task/index.ts

@@ -733,6 +733,17 @@ export class Task {
 			const lastMessage = this.messageStateHandler.getClineMessages().at(-1)
 			const isUpdatingPreviousPartial =
 				lastMessage && lastMessage.partial && lastMessage.type === "say" && lastMessage.say === type
+			const isDuplicateCompletedTextSay =
+				partial &&
+				type === "text" &&
+				lastMessage &&
+				lastMessage.type === "say" &&
+				lastMessage.say === "text" &&
+				!lastMessage.partial &&
+				(lastMessage.text ?? "") === (text ?? "")
+			if (isDuplicateCompletedTextSay) {
+				return undefined
+			}
 			if (partial) {
 				if (isUpdatingPreviousPartial) {
 					// existing partial message, so update it