Skip to content

Commit dfbcc6e

Browse files
author
Zoo (VP)
committed
fix(webview): stop recovery watchdog when sidebar webview is disposed
1 parent 77d02f3 commit dfbcc6e

2 files changed

Lines changed: 43 additions & 21 deletions

File tree

‎src/core/webview/ClineProvider.ts‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,7 @@ export class ClineProvider
820820
*/
821821
private clearWebviewResources() {
822822
this.rejectPendingThemeFixtureProbes(new Error("Webview was disposed before the theme fixture probe completed"))
823+
this.stopWebviewWatchdog()
823824
while (this.webviewDisposables.length) {
824825
const x = this.webviewDisposables.pop()
825826
if (x) {
@@ -846,10 +847,7 @@ export class ClineProvider
846847

847848
this._disposed = true
848849
this._postStateToWebviewThrottled.cancel()
849-
if (this.webviewWatchdogInterval) {
850-
clearInterval(this.webviewWatchdogInterval)
851-
this.webviewWatchdogInterval = null
852-
}
850+
this.stopWebviewWatchdog()
853851
this.log("Disposing ClineProvider...")
854852

855853
// Reject any tasks still waiting for a scheduler permit so they don't
@@ -1143,6 +1141,11 @@ export class ClineProvider
11431141
} else {
11441142
this.log("Clearing webview resources for sidebar view")
11451143
this.clearWebviewResources()
1144+
if (this.view === webviewView) {
1145+
// Drop the disposed view so nothing keeps polling it
1146+
// (e.g. the recovery watchdog) for the provider's lifetime.
1147+
this.view = undefined
1148+
}
11461149
// Reset current workspace manager reference when view is disposed
11471150
this.codeIndexManager = undefined
11481151
}
@@ -3421,6 +3424,14 @@ export class ClineProvider
34213424
}, ClineProvider.WEBVIEW_WATCHDOG_TICK_MS)
34223425
}
34233426

3427+
/** Stops the renderer heartbeat watchdog; the webview it watches is gone. */
3428+
private stopWebviewWatchdog(): void {
3429+
if (this.webviewWatchdogInterval) {
3430+
clearInterval(this.webviewWatchdogInterval)
3431+
this.webviewWatchdogInterval = null
3432+
}
3433+
}
3434+
34243435
/**
34253436
* Reloads only this provider's own webview by regenerating its HTML (fresh
34263437
* nonce) and reassigning `webview.html`, which forces VS Code to reload that

‎src/core/webview/__tests__/ClineProvider.spec.ts‎

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -527,10 +527,7 @@ describe("ClineProvider", () => {
527527
cspSource: "vscode-webview://test-csp-source",
528528
},
529529
visible: true,
530-
onDidDispose: vi.fn().mockImplementation((callback) => {
531-
callback()
532-
return { dispose: vi.fn() }
533-
}),
530+
onDidDispose: vi.fn(),
534531
onDidChangeVisibility: vi.fn().mockImplementation(() => {
535532
return { dispose: vi.fn() }
536533
}),
@@ -665,6 +662,7 @@ describe("ClineProvider", () => {
665662

666663
describe("webview heartbeat watchdog", () => {
667664
let visibilityCallback: () => void
665+
let disposeCallback: () => void
668666

669667
beforeEach(() => {
670668
// Fake timers must be active before resolveWebviewView so the
@@ -674,6 +672,10 @@ describe("ClineProvider", () => {
674672
visibilityCallback = cb
675673
return { dispose: vi.fn() }
676674
})
675+
mockWebviewView.onDidDispose = vi.fn().mockImplementation((cb: () => void) => {
676+
disposeCallback = cb
677+
return { dispose: vi.fn() }
678+
})
677679
})
678680

679681
afterEach(async () => {
@@ -743,6 +745,23 @@ describe("ClineProvider", () => {
743745
expect(mockWebviewView.webview.html).toBe(htmlAfterResolve)
744746
})
745747

748+
test("stops watching and clears the view when the sidebar webview is disposed", async () => {
749+
await provider.resolveWebviewView(mockWebviewView)
750+
const htmlAfterResolve = mockWebviewView.webview.html
751+
752+
// The provider outlives a disposed sidebar view; VS Code re-resolves
753+
// a fresh view later. Disposal must stop the watchdog and drop the
754+
// stale view reference so no recovery reload targets the dead view.
755+
disposeCallback()
756+
expect(provider["webviewWatchdogInterval"]).toBeNull()
757+
// @ts-ignore - accessing private property for testing
758+
expect(provider.view).toBeUndefined()
759+
760+
await vi.advanceTimersByTimeAsync(180_000)
761+
762+
expect(mockWebviewView.webview.html).toBe(htmlAfterResolve)
763+
})
764+
746765
test("does not stack watchdog intervals when resolveWebviewView runs again", async () => {
747766
await provider.resolveWebviewView(mockWebviewView)
748767
await provider.resolveWebviewView(mockWebviewView)
@@ -781,10 +800,7 @@ describe("ClineProvider", () => {
781800
cspSource: "vscode-webview://test-csp-source",
782801
},
783802
visible: true,
784-
onDidDispose: vi.fn().mockImplementation((callback: () => void) => {
785-
callback()
786-
return { dispose: vi.fn() }
787-
}),
803+
onDidDispose: vi.fn(),
788804
onDidChangeVisibility: vi.fn().mockImplementation(() => ({ dispose: vi.fn() })),
789805
} as unknown as vscode.WebviewView
790806
const providerB = new ClineProvider(
@@ -801,7 +817,8 @@ describe("ClineProvider", () => {
801817
const htmlBeforeB = mockWebviewViewB.webview.html
802818

803819
await vi.advanceTimersByTimeAsync(119_000)
804-
// Keep provider B's heartbeat fresh so only A's watchdog fires.
820+
// B's view is still alive; refresh its heartbeat so a stale one
821+
// would reload it too, leaving only A's watchdog to fire.
805822
await webviewMessageHandler(providerB, { type: "webviewHeartbeat", timestamp: Date.now() })
806823
await vi.advanceTimersByTimeAsync(1_000)
807824

@@ -3867,10 +3884,7 @@ describe("ClineProvider - Router Models", () => {
38673884
asWebviewUri: vi.fn(),
38683885
},
38693886
visible: true,
3870-
onDidDispose: vi.fn().mockImplementation((callback) => {
3871-
callback()
3872-
return { dispose: vi.fn() }
3873-
}),
3887+
onDidDispose: vi.fn(),
38743888
onDidChangeVisibility: vi.fn().mockImplementation(() => {
38753889
return { dispose: vi.fn() }
38763890
}),
@@ -4221,10 +4235,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
42214235
asWebviewUri: vi.fn(),
42224236
},
42234237
visible: true,
4224-
onDidDispose: vi.fn().mockImplementation((callback) => {
4225-
callback()
4226-
return { dispose: vi.fn() }
4227-
}),
4238+
onDidDispose: vi.fn(),
42284239
onDidChangeVisibility: vi.fn().mockImplementation(() => {
42294240
return { dispose: vi.fn() }
42304241
}),

0 commit comments

Comments
 (0)