Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
inputState: {
groups: [],
sessionResource: undefined,
onDidDispose: Event.None,
onDidChange: Event.None
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ beforeAll(() => {
groups,
sessionResource: undefined,
onDidChange: emitter.event,
onDidDispose: Event.None,
};
// Proxy that fires onDidChange when groups are replaced
return new Proxy(state, {
Expand Down Expand Up @@ -325,7 +326,7 @@ async function runHandlerAndCapture(
resource: ClaudeSessionUri.forSessionId(sessionId),
label: 'Test Session',
},
inputState: { groups, sessionResource: undefined, onDidChange: Event.None },
inputState: { groups, sessionResource: undefined, onDidChange: Event.None, onDidDispose: Event.None },
},
} as vscode.ChatContext;

Expand Down Expand Up @@ -660,7 +661,7 @@ describe('ChatSessionContentProvider', () => {
resource: ClaudeSessionUri.forSessionId(sessionId),
label: 'Test Session',
},
inputState: { groups: buildInputStateGroups(options), sessionResource: undefined, onDidChange: Event.None },
inputState: { groups: buildInputStateGroups(options), sessionResource: undefined, onDidChange: Event.None, onDidDispose: Event.None },
},
} as vscode.ChatContext;
}
Expand Down Expand Up @@ -775,6 +776,7 @@ describe('ChatSessionContentProvider', () => {
}),
sessionResource: undefined,
onDidChange: Event.None,
onDidDispose: Event.None,
},
},
} as vscode.ChatContext;
Expand Down Expand Up @@ -845,7 +847,7 @@ describe('ChatSessionContentProvider', () => {
resource: ClaudeSessionUri.forSessionId(sessionId),
label: 'Test Session',
},
inputState: { groups: buildInputStateGroups(), sessionResource: undefined, onDidChange: Event.None },
inputState: { groups: buildInputStateGroups(), sessionResource: undefined, onDidChange: Event.None, onDidDispose: Event.None },
},
} as vscode.ChatContext;
}
Expand Down Expand Up @@ -945,7 +947,7 @@ describe('ChatSessionContentProvider', () => {
resource: ClaudeSessionUri.forSessionId(sessionId),
label: 'Test Session',
},
inputState: { groups: buildInputStateGroups(), sessionResource: undefined, onDidChange: Event.None },
inputState: { groups: buildInputStateGroups(), sessionResource: undefined, onDidChange: Event.None, onDidDispose: Event.None },
},
} as vscode.ChatContext;
}
Expand Down Expand Up @@ -1163,6 +1165,7 @@ describe('ChatSessionContentProvider', () => {
groups: lockedGroups,
sessionResource: undefined,
onDidChange: Event.None,
onDidDispose: Event.None,
};
// sanity check
expect(initialGroup.items.map(i => i.id)).toEqual([folderA.fsPath, folderB.fsPath]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function makeRef(name: string, type: number = 0 /* Head */): { name: string; typ

function createMockChatSessionInputState(groups: readonly vscode.ChatSessionProviderOptionGroup[]): vscode.ChatSessionInputState {
return {
onDidDispose: Event.None,
onDidChange: Event.None,
groups,
sessionResource: undefined
Expand Down
48 changes: 46 additions & 2 deletions src/vs/workbench/api/common/extHostChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { IExtHostRpcService } from './extHostRpcService.js';
import * as typeConvert from './extHostTypeConverters.js';
import { Diagnostic } from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { isEqual } from '../../../base/common/resources.js';

type ChatSessionTiming = vscode.ChatSessionItem['timing'];

Expand All @@ -44,6 +45,9 @@ class ChatSessionInputStateImpl implements vscode.ChatSessionInputState {
readonly #onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.#onDidChangeEmitter.event;

readonly #onDidDisposeEmitter = new Emitter<void>();
readonly onDidDispose = this.#onDidDisposeEmitter.event;

#sessionResource: vscode.Uri | undefined;
get sessionResource(): vscode.Uri | undefined {
return this.#sessionResource;
Expand Down Expand Up @@ -81,6 +85,12 @@ class ChatSessionInputStateImpl implements vscode.ChatSessionInputState {
_setGroups(groups: readonly vscode.ChatSessionProviderOptionGroup[]): void {
this.#groups = groups;
}

_dispose(): void {
this.#onDidDisposeEmitter.fire();
this.#onDidDisposeEmitter.dispose();
this.#onDidChangeEmitter.dispose();
}
}

// #endregion
Expand Down Expand Up @@ -579,6 +589,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
},
dispose: () => {
isDisposed = true;
for (const inputState of inputStates) {
inputState._dispose();
}
inputStates.clear();
disposables.dispose();
},
});
Expand Down Expand Up @@ -652,6 +666,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
);

if (inputState instanceof ChatSessionInputStateImpl) {
// Dispose any previous input states for this session resource
if (controllerData) {
this._disposeInputStatesForResource(controllerData.inputStates, sessionResource);
}

if (isUntitledChatSession(sessionResource)) {
inputState.untitledSessionResource = sessionResource;
} else {
Expand Down Expand Up @@ -805,15 +824,22 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}

async $disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise<void> {
const entry = this._extHostChatSessions.get(URI.revive(sessionResource));
const resource = URI.revive(sessionResource);
const entry = this._extHostChatSessions.get(resource);
if (!entry) {
this._logService.warn(`No chat session found for resource: ${sessionResource}`);
return;
}

// Dispose input states associated with this session
const controllerData = this.getChatSessionItemController(resource.scheme);
if (controllerData) {
this._disposeInputStatesForResource(controllerData.inputStates, resource);
}

entry.disposeCts.cancel();
entry.sessionObj.sessionDisposables.dispose();
this._extHostChatSessions.delete(URI.revive(sessionResource));
this._extHostChatSessions.delete(resource);
}

async $invokeChatSessionRequestHandler(handle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult> {
Expand Down Expand Up @@ -882,6 +908,16 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
return undefined;
}

private _disposeInputStatesForResource(inputStates: Set<ChatSessionInputStateImpl>, resource: URI): void {
for (const inputState of inputStates) {
const inputResource = inputState.sessionResource ?? inputState.untitledSessionResource;
if (inputResource && isEqual(resource, inputResource)) {
inputState._dispose();
inputStates.delete(inputState);
}
}
}

private _createInputStateFromOptions(
groups: readonly vscode.ChatSessionProviderOptionGroup[],
sessionOptions?: ReadonlyArray<{ optionId: string; value: string }>,
Expand Down Expand Up @@ -924,6 +960,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
);
if (result) {
if (result instanceof ChatSessionInputStateImpl) {
// Dispose any previous input states for this session resource
if (sessionResource && controllerData) {
this._disposeInputStatesForResource(controllerData.inputStates, sessionResource);
}

Comment on lines +963 to +967
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposing prior input states here (inside getInputStateForSession, which is used by the option-group proxy command wrapper) can dispose the active ChatSessionInputStateImpl that the UI/extension is still using to receive onDidChange updates for the session. This call path is triggered when a user runs an option-group command and should not tear down the session’s live input state. Suggest removing the _disposeInputStatesForResource(...) call from this method (or changing the command wrapper to use an untracked/snapshot input state) and only disposing/replacing states in the session lifecycle paths ($provideChatSessionContent / $provideChatSessionInputState / $disposeChatSessionContent).

Suggested change
// Dispose any previous input states for this session resource
if (sessionResource && controllerData) {
this._disposeInputStatesForResource(controllerData.inputStates, sessionResource);
}

Copilot uses AI. Check for mistakes.
if (sessionResource && isUntitledChatSession(sessionResource)) {
result.untitledSessionResource = sessionResource;
} else if (sessionResource) {
Expand Down Expand Up @@ -1193,6 +1234,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}

if (inputState instanceof ChatSessionInputStateImpl && sessionResource) {
// Dispose any previous input states for this session resource
this._disposeInputStatesForResource(controllerData.inputStates, sessionResource);

if (isUntitledChatSession(sessionResource)) {
inputState.untitledSessionResource = sessionResource;
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,11 @@ declare module 'vscode' {
* Represents the current state of user inputs for a chat session.
*/
export interface ChatSessionInputState {
/**
* Fired when the input state is disposed.
*/
readonly onDidDispose: Event<void>;

/**
* Fired when the input state is changed by the user.
*
Expand Down
Loading