From b9a9c60f08172078319269b82cadd02e67f93392 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 31 Aug 2026 23:31:25 +0200 Subject: [PATCH] sessions: Avoid resolving canceled Changes editor inputs A canceled setInput operation can outlive editor cleanup and try to resolve an already disposed SessionChangesEditorInput. Stop before model resolution and cover the race with a regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../changes/browser/sessionChangesEditor.ts | 3 ++ .../browser/sessionChangesEditorInput.test.ts | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/vs/sessions/contrib/changes/browser/sessionChangesEditor.ts b/src/vs/sessions/contrib/changes/browser/sessionChangesEditor.ts index fb59f5af11da6..214f63182c7b6 100644 --- a/src/vs/sessions/contrib/changes/browser/sessionChangesEditor.ts +++ b/src/vs/sessions/contrib/changes/browser/sessionChangesEditor.ts @@ -301,6 +301,9 @@ export class SessionChangesEditor extends AbstractEditorWithViewState { await super.setInput(input, options, context, token); + if (token.isCancellationRequested) { + return; + } const sessionResource = this.sessionChangesService.getSessionResource(input.multiDiffSource); this._inputSessionResource.set(sessionResource, undefined); const viewModel = await input.getViewModel(); diff --git a/src/vs/sessions/contrib/changes/test/browser/sessionChangesEditorInput.test.ts b/src/vs/sessions/contrib/changes/test/browser/sessionChangesEditorInput.test.ts index ff6a18689a7d4..6fff73fd0706b 100644 --- a/src/vs/sessions/contrib/changes/test/browser/sessionChangesEditorInput.test.ts +++ b/src/vs/sessions/contrib/changes/test/browser/sessionChangesEditorInput.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter, Event, ValueWithChangeEvent } from '../../../../../base/common/event.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; @@ -112,6 +113,38 @@ suite('SessionChangesEditorInput', () => { }); }); + test('does not resolve a canceled editor input', async () => { + class TestSessionChangesEditorInput extends SessionChangesEditorInput { + viewModelRequested = false; + + override async getViewModel(): Promise { + this.viewModelRequested = true; + throw new Error('Canceled input must not be resolved'); + } + } + + const instantiationService = workbenchInstantiationService(undefined, disposables); + instantiationService.stub(IChangesViewService, {}); + instantiationService.stub(IAgentWorkbenchLayoutService, {}); + instantiationService.stub(ISessionChangesService, {}); + instantiationService.stub(IWorkbenchLayoutService, { + onDidChangePartVisibility: Event.None, + isVisible: () => true, + }); + + const editor = disposables.add(instantiationService.createInstance(SessionChangesEditor, new TestEditorGroupView(1))); + const input = disposables.add(instantiationService.createInstance( + TestSessionChangesEditorInput, + URI.parse('changes-multi-diff-source:?{"sessionResource":"agent-host-copilotcli:/session"}'), + )); + const operation = disposables.add(new CancellationTokenSource()); + operation.cancel(); + + await editor.setInput(input, undefined, {}, operation.token); + + assert.deepStrictEqual(input.viewModelRequested, false); + }); + test('updates managed Changes editor capabilities with editor area visibility', () => { const instantiationService = disposables.add(new TestInstantiationService()); let editorVisible = false;