@@ -219,7 +219,6 @@ export class ClineProvider
219219 public static readonly sideBarId = `${ Package . name } .SidebarProvider`
220220 public static readonly tabPanelId = `${ Package . name } .TabPanelProvider`
221221 private static activeInstances : Set < ClineProvider > = new Set ( )
222- private disposables : vscode . Disposable [ ] = [ ]
223222 private webviewDisposables : vscode . Disposable [ ] = [ ]
224223 private pendingThemeFixtureProbes = new Map <
225224 string ,
@@ -837,6 +836,15 @@ export class ClineProvider
837836 }
838837 }
839838
839+ /**
840+ * True when an in-flight resolve no longer belongs to the provider's current
841+ * view because the view was disposed or replaced (or the provider itself was
842+ * disposed) while the resolve was awaiting HTML or state hydration.
843+ */
844+ private isStaleViewResolve ( webviewView : vscode . WebviewView | vscode . WebviewPanel ) : boolean {
845+ return this . _disposed || this . view !== webviewView
846+ }
847+
840848 /** Drain one task's memoized cleanup without preventing the remaining provider shutdown work. */
841849 private async drainTaskDisposal ( task : Task ) : Promise < void > {
842850 try {
@@ -893,14 +901,6 @@ export class ClineProvider
893901 CloudService . instance . off ( "settings-updated" , this . handleCloudSettingsUpdate )
894902 }
895903
896- while ( this . disposables . length ) {
897- const x = this . disposables . pop ( )
898-
899- if ( x ) {
900- x . dispose ( )
901- }
902- }
903-
904904 this . _workspaceTracker ?. dispose ( )
905905 this . _workspaceTracker = undefined
906906 await this . mcpHub ?. unregisterClient ( )
@@ -1030,9 +1030,40 @@ export class ClineProvider
10301030 }
10311031
10321032 async resolveWebviewView ( webviewView : vscode . WebviewView | vscode . WebviewPanel ) {
1033+ // A different view replaces the current one: dispose its subscriptions so
1034+ // the replaced view can no longer dispatch messages or refresh provider state.
1035+ if ( this . view !== webviewView ) {
1036+ this . clearWebviewResources ( )
1037+ }
1038+
10331039 this . view = webviewView
10341040 const inTabMode = "onDidChangeViewState" in webviewView
10351041
1042+ // Register disposal before awaiting HTML or state so closing the view during
1043+ // either load invalidates this resolve and prevents later listener installation.
1044+ webviewView . onDidDispose (
1045+ async ( ) => {
1046+ // A view that is no longer current must not clear the current
1047+ // view's resources or dispose the provider.
1048+ if ( webviewView !== this . view ) {
1049+ return
1050+ }
1051+
1052+ if ( inTabMode ) {
1053+ this . log ( "Disposing ClineProvider instance for tab view" )
1054+ await this . dispose ( )
1055+ } else {
1056+ this . log ( "Clearing webview resources for sidebar view" )
1057+ this . view = undefined
1058+ this . clearWebviewResources ( )
1059+ }
1060+ } ,
1061+ null ,
1062+ // View-scoped, like the other view subscriptions: disposed both when
1063+ // the view is disposed and when a different view replaces it.
1064+ this . webviewDisposables ,
1065+ )
1066+
10361067 if ( inTabMode ) {
10371068 setPanel ( webviewView , "tab" )
10381069 } else if ( "onDidChangeVisibility" in webviewView ) {
@@ -1052,12 +1083,20 @@ export class ClineProvider
10521083 localResourceRoots : resourceRoots ,
10531084 }
10541085
1055- webviewView . webview . html =
1086+ const html =
10561087 this . contextProxy . extensionMode === vscode . ExtensionMode . Development &&
10571088 process . env . ROO_CODE_THEME_FIXTURE_PROBE !== "1"
10581089 ? await this . getHMRHtmlContent ( webviewView . webview )
10591090 : await this . getHtmlContent ( webviewView . webview )
10601091
1092+ // The HTML load above can finish after the view was disposed or replaced.
1093+ // A stale resolve must not assign HTML to a view the provider no longer owns.
1094+ if ( this . isStaleViewResolve ( webviewView ) ) {
1095+ return
1096+ }
1097+
1098+ webviewView . webview . html = html
1099+
10611100 // Initialize out-of-scope variables that need to receive persistent
10621101 // global state values.
10631102 await this . getState ( ) . then (
@@ -1088,9 +1127,15 @@ export class ClineProvider
10881127 } ,
10891128 )
10901129
1130+ // State hydration can also finish after the view was disposed or replaced.
1131+ // A stale resolve must not install listeners on a view the provider no longer owns.
1132+ if ( this . isStaleViewResolve ( webviewView ) ) {
1133+ return
1134+ }
1135+
10911136 // Sets up an event listener to listen for messages passed from the webview view context
10921137 // and executes code based on the message that is received.
1093- this . setWebviewMessageListener ( webviewView . webview )
1138+ this . setWebviewMessageListener ( webviewView )
10941139 this . webviewDisposables . push ( this . webviewFocusTracker . init ( this , webviewView ) )
10951140
10961141 // Listen for when the panel becomes visible.
@@ -1099,6 +1144,11 @@ export class ClineProvider
10991144 // WebviewView and WebviewPanel have all the same properties except
11001145 // for this visibility listener panel.
11011146 const viewStateDisposable = webviewView . onDidChangeViewState ( ( ) => {
1147+ // Ignore events from a view that is no longer the current view.
1148+ if ( webviewView !== this . view ) {
1149+ return
1150+ }
1151+
11021152 if ( this . view ?. visible ) {
11031153 void this . postMessageToWebview ( { type : "action" , action : "didBecomeVisible" } )
11041154 } else {
@@ -1110,6 +1160,11 @@ export class ClineProvider
11101160 } else if ( "onDidChangeVisibility" in webviewView ) {
11111161 // sidebar
11121162 const visibilityDisposable = webviewView . onDidChangeVisibility ( ( ) => {
1163+ // Ignore events from a view that is no longer the current view.
1164+ if ( webviewView !== this . view ) {
1165+ return
1166+ }
1167+
11131168 if ( this . view ?. visible ) {
11141169 void this . postMessageToWebview ( { type : "action" , action : "didBecomeVisible" } )
11151170 } else {
@@ -1120,24 +1175,13 @@ export class ClineProvider
11201175 this . webviewDisposables . push ( visibilityDisposable )
11211176 }
11221177
1123- // Listen for when the view is disposed
1124- // This happens when the user closes the view or when the view is closed programmatically
1125- webviewView . onDidDispose (
1126- async ( ) => {
1127- if ( inTabMode ) {
1128- this . log ( "Disposing ClineProvider instance for tab view" )
1129- await this . dispose ( )
1130- } else {
1131- this . log ( "Clearing webview resources for sidebar view" )
1132- this . clearWebviewResources ( )
1133- }
1134- } ,
1135- null ,
1136- this . disposables ,
1137- )
1138-
11391178 // Listen for when color changes
11401179 const configDisposable = vscode . workspace . onDidChangeConfiguration ( async ( e ) => {
1180+ // Ignore configuration refreshes scoped to a view that is no longer current.
1181+ if ( webviewView !== this . view ) {
1182+ return
1183+ }
1184+
11411185 if ( e && e . affectsConfiguration ( "workbench.colorTheme" ) ) {
11421186 // Sends latest theme name to webview
11431187 await this . postMessageToWebview ( { type : "theme" , text : JSON . stringify ( await getTheme ( ) ) } )
@@ -1704,13 +1748,19 @@ export class ClineProvider
17041748 * Sets up an event listener to listen for messages passed from the webview context and
17051749 * executes code based on the message that is received.
17061750 *
1707- * @param webview A reference to the extension webview
1751+ * @param webviewView The view whose webview messages are listened to
17081752 */
1709- private setWebviewMessageListener ( webview : vscode . Webview ) {
1710- const onReceiveMessage = async ( message : WebviewMessage ) =>
1711- webviewMessageHandler ( this , message , this . marketplaceManager )
1753+ private setWebviewMessageListener ( webviewView : vscode . WebviewView | vscode . WebviewPanel ) {
1754+ const onReceiveMessage = async ( message : WebviewMessage ) => {
1755+ // Ignore messages from a view that is no longer the current view.
1756+ if ( webviewView !== this . view ) {
1757+ return
1758+ }
1759+
1760+ await webviewMessageHandler ( this , message , this . marketplaceManager )
1761+ }
17121762
1713- const messageDisposable = webview . onDidReceiveMessage ( onReceiveMessage )
1763+ const messageDisposable = webviewView . webview . onDidReceiveMessage ( onReceiveMessage )
17141764 this . webviewDisposables . push ( messageDisposable )
17151765 }
17161766
0 commit comments