From 5d46d8fc01a54920fad873744aff4f706a9a5b90 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 01:13:28 +0000 Subject: [PATCH 1/3] fix(webview): preserve task state publication order --- src/core/webview/ClineProvider.ts | 8 ++--- .../webview/__tests__/ClineProvider.spec.ts | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 1d232d1dcb..686c01fd93 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2398,9 +2398,9 @@ export class ClineProvider } async postStateToWebview() { + const clineMessagesSeq = ++this.clineMessagesSeq const state = await this.getStateToPostToWebview() - this.clineMessagesSeq++ - state.clineMessagesSeq = this.clineMessagesSeq + state.clineMessagesSeq = clineMessagesSeq await this.postMessageToWebview({ type: "state", state }) } @@ -2413,9 +2413,9 @@ export class ClineProvider * `taskHistoryUpdated` / `taskHistoryItemUpdated`. */ async postStateToWebviewWithoutTaskHistory(): Promise { + const clineMessagesSeq = ++this.clineMessagesSeq const state = await this.getStateToPostToWebview({ includeTaskHistory: false }) - this.clineMessagesSeq++ - state.clineMessagesSeq = this.clineMessagesSeq + state.clineMessagesSeq = clineMessagesSeq const { taskHistory: _omit, ...rest } = state await this.postMessageToWebview({ type: "state", state: rest }) } diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 731124cccc..8a2d5d5923 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -884,6 +884,42 @@ describe("ClineProvider", () => { expect(statePostSettled).toBe(true) }) + test.each([ + ["postStateToWebview", (currentProvider: ClineProvider) => currentProvider.postStateToWebview()], + [ + "postStateToWebviewWithoutTaskHistory", + (currentProvider: ClineProvider) => currentProvider.postStateToWebviewWithoutTaskHistory(), + ], + ])("%s assigns message sequence numbers before asynchronous state construction", async (_methodName, postState) => { + let releaseOlderSnapshot!: (state: ExtensionState) => void + const olderSnapshot = new Promise((resolve) => { + releaseOlderSnapshot = resolve + }) + const emptyState = { taskHistory: [], clineMessages: [] } as unknown as ExtensionState + const readyState = { + taskHistory: [], + clineMessages: [{ type: "say", say: "text", text: "child ready" }], + } as unknown as ExtensionState + + vi.spyOn(provider, "getStateToPostToWebview") + .mockReturnValueOnce(olderSnapshot) + .mockResolvedValueOnce(readyState) + const postMessageSpy = vi.spyOn(provider, "postMessageToWebview").mockResolvedValue(undefined) + + const olderPost = postState(provider) + await Promise.resolve() + const newerPost = postState(provider) + await newerPost + releaseOlderSnapshot(emptyState) + await olderPost + + expect(postMessageSpy.mock.calls.map(([message]) => message.state?.clineMessages)).toEqual([ + readyState.clineMessages, + emptyState.clineMessages, + ]) + expect(postMessageSpy.mock.calls.map(([message]) => message.state?.clineMessagesSeq)).toEqual([2, 1]) + }) + test.each([ [ "postStateToWebviewWithoutTaskHistory", From 9262999e5121417475a08c1483b9de192f33bda4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 02:51:39 +0000 Subject: [PATCH 2/3] test(webview): use complete state fixtures --- src/core/webview/__tests__/ClineProvider.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 8a2d5d5923..cd9c4f42ac 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -895,11 +895,13 @@ describe("ClineProvider", () => { const olderSnapshot = new Promise((resolve) => { releaseOlderSnapshot = resolve }) - const emptyState = { taskHistory: [], clineMessages: [] } as unknown as ExtensionState - const readyState = { + const baseState = await provider.getStateToPostToWebview({ includeTaskHistory: false }) + const emptyState: ExtensionState = { ...baseState, taskHistory: [], clineMessages: [] } + const readyState: ExtensionState = { + ...baseState, taskHistory: [], clineMessages: [{ type: "say", say: "text", text: "child ready" }], - } as unknown as ExtensionState + } vi.spyOn(provider, "getStateToPostToWebview") .mockReturnValueOnce(olderSnapshot) From 83aa382a1e8a88a505db19c24b885cd44fccaef4 Mon Sep 17 00:00:00 2001 From: Roomote Date: Mon, 31 Aug 2026 02:54:21 +0000 Subject: [PATCH 3/3] test(webview): complete message fixture --- src/core/webview/__tests__/ClineProvider.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cd9c4f42ac..ad6ea143a8 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -900,7 +900,7 @@ describe("ClineProvider", () => { const readyState: ExtensionState = { ...baseState, taskHistory: [], - clineMessages: [{ type: "say", say: "text", text: "child ready" }], + clineMessages: [{ ts: 1, type: "say", say: "text", text: "child ready" }], } vi.spyOn(provider, "getStateToPostToWebview")