@@ -30,6 +30,7 @@ type TaskTestAccess = {
3030 startTask : ( task ?: string , images ?: string [ ] ) => Promise < void >
3131 resumeTaskFromHistory : ( ) => Promise < void >
3232 presentAssistantMessageSafe : ( ) => void
33+ addToClineMessages : ( message : import ( "@roo-code/types" ) . ClineMessage ) => Promise < void >
3334 updateClineMessage : ( message : import ( "@roo-code/types" ) . ClineMessage ) => Promise < void >
3435 saveClineMessages : ( ) => Promise < boolean >
3536 safeEnsureModelFetched : ( ) => Promise < void >
@@ -338,6 +339,8 @@ describe("Cline", () => {
338339 mockProvider . postMessageToWebview = vi . fn ( ) . mockResolvedValue ( undefined )
339340 mockProvider . postStateToWebview = vi . fn ( ) . mockResolvedValue ( undefined )
340341 mockProvider . postStateToWebviewWithoutTaskHistory = vi . fn ( ) . mockResolvedValue ( undefined )
342+ mockProvider . postStateToWebviewThrottled = vi . fn ( ) . mockResolvedValue ( undefined )
343+ mockProvider . flushPostStateToWebviewThrottled = vi . fn ( ) . mockResolvedValue ( undefined )
341344 mockProvider . getTaskWithId = vi . fn ( ) . mockImplementation ( async ( id ) => ( {
342345 historyItem : {
343346 id,
@@ -1029,6 +1032,8 @@ describe("Cline", () => {
10291032 say : vi . fn ( ) ,
10301033 postStateToWebview : vi . fn ( ) . mockResolvedValue ( undefined ) ,
10311034 postStateToWebviewWithoutTaskHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1035+ postStateToWebviewThrottled : vi . fn ( ) . mockResolvedValue ( undefined ) ,
1036+ flushPostStateToWebviewThrottled : vi . fn ( ) . mockResolvedValue ( undefined ) ,
10321037 postMessageToWebview : vi . fn ( ) . mockResolvedValue ( undefined ) ,
10331038 updateTaskHistory : vi . fn ( ) . mockResolvedValue ( undefined ) ,
10341039 }
@@ -1663,6 +1668,78 @@ describe("Cline", () => {
16631668 } )
16641669 } )
16651670
1671+ describe ( "webview state throttling" , ( ) => {
1672+ it ( "schedules a complete new message without forcing an immediate state push" , async ( ) => {
1673+ const task = new Task ( {
1674+ provider : mockProvider ,
1675+ apiConfiguration : mockApiConfig ,
1676+ task : "test task" ,
1677+ startTask : false ,
1678+ } )
1679+ vi . spyOn ( getTaskTestAccess ( task ) , "saveClineMessages" ) . mockResolvedValue ( true )
1680+ const message = {
1681+ ts : Date . now ( ) ,
1682+ type : "say" as const ,
1683+ say : "text" as const ,
1684+ text : "message" ,
1685+ }
1686+
1687+ await getTaskTestAccess ( task ) . addToClineMessages ( message )
1688+
1689+ expect ( mockProvider . postStateToWebviewThrottled ) . toHaveBeenCalledOnce ( )
1690+ expect ( mockProvider . postStateToWebviewThrottled ) . toHaveBeenCalledWith ( )
1691+ expect ( mockProvider . flushPostStateToWebviewThrottled ) . not . toHaveBeenCalled ( )
1692+ expect ( mockProvider . postStateToWebviewWithoutTaskHistory ) . not . toHaveBeenCalled ( )
1693+ } )
1694+
1695+ it ( "waits for a new partial message flush before a following message update" , async ( ) => {
1696+ const task = new Task ( {
1697+ provider : mockProvider ,
1698+ apiConfiguration : mockApiConfig ,
1699+ task : "test task" ,
1700+ startTask : false ,
1701+ } )
1702+ const taskAccess = getTaskTestAccess ( task )
1703+ vi . spyOn ( taskAccess , "saveClineMessages" ) . mockResolvedValue ( true )
1704+ let releaseFlush ! : ( ) => void
1705+ const pendingFlush = new Promise < void > ( ( resolve ) => {
1706+ releaseFlush = resolve
1707+ } )
1708+ const flushSpy = vi . mocked ( mockProvider . flushPostStateToWebviewThrottled ) . mockReturnValueOnce ( pendingFlush )
1709+ const updatePostSpy = vi . mocked ( mockProvider . postMessageToWebview )
1710+ const partialMessage = {
1711+ ts : 1 ,
1712+ type : "say" as const ,
1713+ say : "text" as const ,
1714+ text : "partial message" ,
1715+ partial : true ,
1716+ }
1717+ let partialAddSettled = false
1718+ const addThenUpdate = taskAccess . addToClineMessages ( partialMessage ) . then ( async ( ) => {
1719+ partialAddSettled = true
1720+ await taskAccess . updateClineMessage ( { ...partialMessage , text : "updated partial" } )
1721+ } )
1722+
1723+ await Promise . resolve ( )
1724+ expect ( mockProvider . postStateToWebviewThrottled ) . toHaveBeenCalledWith ( )
1725+ expect ( flushSpy ) . toHaveBeenCalledWith ( )
1726+ expect ( partialAddSettled ) . toBe ( false )
1727+ expect ( updatePostSpy ) . not . toHaveBeenCalled ( )
1728+
1729+ releaseFlush ( )
1730+ await addThenUpdate
1731+
1732+ expect ( flushSpy . mock . invocationCallOrder [ 0 ] ) . toBeLessThan ( updatePostSpy . mock . invocationCallOrder [ 0 ] )
1733+ expect ( updatePostSpy ) . toHaveBeenCalledWith ( {
1734+ type : "messageUpdated" ,
1735+ clineMessage : {
1736+ ...partialMessage ,
1737+ text : "updated partial" ,
1738+ } ,
1739+ } )
1740+ } )
1741+ } )
1742+
16661743 describe ( "abortTask" , ( ) => {
16671744 it ( "should set abort flag and emit TaskAborted event" , async ( ) => {
16681745 const task = new Task ( {
@@ -1707,6 +1784,37 @@ describe("Cline", () => {
17071784 expect ( disposeSpy ) . toHaveBeenCalled ( )
17081785 } )
17091786
1787+ it ( "flushes pending state before TaskAborted and disposal while queue state is intact" , async ( ) => {
1788+ const task = new Task ( {
1789+ provider : mockProvider ,
1790+ apiConfiguration : mockApiConfig ,
1791+ task : "test task" ,
1792+ startTask : false ,
1793+ } )
1794+ let queuedMessagesAtFlush = - 1
1795+ const flushSpy = vi . mocked ( mockProvider . flushPostStateToWebviewThrottled ) . mockImplementation ( async ( ) => {
1796+ queuedMessagesAtFlush = task . messageQueueService . messages . length
1797+ } )
1798+ const emitSpy = vi . spyOn ( task , "emit" )
1799+ const disposeSpy = vi . spyOn ( task , "dispose" ) . mockImplementation ( ( ) => { } )
1800+
1801+ task . messageQueueService . addMessage ( "queued text" )
1802+ await task . abortTask ( )
1803+
1804+ const taskAbortedCallIndex = ( emitSpy . mock . calls as unknown [ ] [ ] ) . findIndex (
1805+ ( [ event ] ) => event === "taskAborted" ,
1806+ )
1807+ expect ( taskAbortedCallIndex ) . toBeGreaterThanOrEqual ( 0 )
1808+ expect ( queuedMessagesAtFlush ) . toBe ( 1 )
1809+ expect ( flushSpy ) . toHaveBeenCalledWith ( )
1810+ expect ( flushSpy . mock . invocationCallOrder [ 0 ] ) . toBeLessThan (
1811+ emitSpy . mock . invocationCallOrder [ taskAbortedCallIndex ] ,
1812+ )
1813+ expect ( emitSpy . mock . invocationCallOrder [ taskAbortedCallIndex ] ) . toBeLessThan (
1814+ disposeSpy . mock . invocationCallOrder [ 0 ] ,
1815+ )
1816+ } )
1817+
17101818 it ( "should work with TaskLike interface" , async ( ) => {
17111819 const task = new Task ( {
17121820 provider : mockProvider ,
@@ -2859,9 +2967,9 @@ describe("Cline", () => {
28592967 resumeSpy . mockRestore ( )
28602968 } )
28612969
2862- it ( "logs (instead of crashing) when postStateToWebviewWithoutTaskHistory rejects from the queue handler" , async ( ) => {
2970+ it ( "logs (instead of crashing) when postStateToWebviewThrottled rejects from the queue handler" , async ( ) => {
28632971 const boom = new Error ( "postState boom" )
2864- mockProvider . postStateToWebviewWithoutTaskHistory = vi . fn ( ) . mockRejectedValue ( boom )
2972+ mockProvider . postStateToWebviewThrottled = vi . fn ( ) . mockRejectedValue ( boom )
28652973
28662974 const task = new Task ( {
28672975 provider : mockProvider ,
@@ -2870,13 +2978,14 @@ describe("Cline", () => {
28702978 startTask : false ,
28712979 } )
28722980
2873- // Triggers messageQueueStateChangedHandler -> void postStateToWebviewWithoutTaskHistory ()
2981+ // Triggers messageQueueStateChangedHandler -> void postStateToWebviewThrottled ()
28742982 task . messageQueueService . addMessage ( "queued text" )
28752983 await flushMicrotasks ( )
28762984
2877- expect ( mockProvider . postStateToWebviewWithoutTaskHistory ) . toHaveBeenCalled ( )
2985+ expect ( mockProvider . postStateToWebviewThrottled ) . toHaveBeenCalledWith ( )
2986+ expect ( mockProvider . postStateToWebviewWithoutTaskHistory ) . not . toHaveBeenCalled ( )
28782987 expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
2879- "[Task#messageQueueStateChangedHandler] postStateToWebviewWithoutTaskHistory failed:" ,
2988+ "[Task#messageQueueStateChangedHandler] postStateToWebviewThrottled failed:" ,
28802989 boom ,
28812990 )
28822991 } )
0 commit comments