Skip to content

Commit 73991f0

Browse files
DonJayamanneCopilot
andauthored
Fix model selection logic in SessionModelPicker to handle empty models and active session changes (#313394)
* Fix model selection logic in SessionModelPicker to handle empty models and active session changes * Updates Co-authored-by: Copilot <copilot@github.com> * Updates Co-authored-by: Copilot <copilot@github.com> * new tests --------- Co-authored-by: Copilot <copilot@github.com>
1 parent df29bd2 commit 73991f0

2 files changed

Lines changed: 67 additions & 5 deletions

File tree

src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsActions.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export class SessionModelPicker extends Disposable {
277277
private readonly _delegate: IModelPickerDelegate;
278278
private readonly _modelPicker: ModelPickerActionItem;
279279
private _lastSessionType: string | undefined;
280+
private _lastPushedSessionId: string | undefined;
280281

281282
constructor(
282283
@IInstantiationService instantiationService: IInstantiationService,
@@ -338,10 +339,28 @@ export class SessionModelPicker extends Disposable {
338339

339340
const models = getAvailableModels(this._languageModelsService, this._sessionsManagementService);
340341
this._modelPicker.setEnabled(models.length > 0);
341-
if (!this._currentModel.get() && models.length > 0) {
342+
if (models.length === 0) {
343+
return;
344+
}
345+
346+
const current = this._currentModel.get();
347+
if (!current) {
342348
const rememberedModelId = sessionType ? this._storageService.get(modelPickerStorageKey(sessionType), StorageScope.PROFILE) : undefined;
343349
const remembered = rememberedModelId ? models.find(m => m.identifier === rememberedModelId) : undefined;
344350
this._delegate.setModel(remembered ?? models[0]);
351+
this._lastPushedSessionId = session?.sessionId;
352+
} else if (session && session.sessionId !== this._lastPushedSessionId && models.some(m => m.identifier === current.identifier)) {
353+
// Active session changed (e.g. user switched repository) but the
354+
// previously selected model is still available. Re-push it so the
355+
// new session's provider receives setModel — otherwise the request
356+
// would be sent with the default model even though the picker UI
357+
// still shows the user's selection. See #313385.
358+
//
359+
// Gated on sessionId so unrelated re-invocations of _initModel
360+
// (e.g. from onDidChangeLanguageModels) don't redundantly write
361+
// storage and dispatch provider.setModel for the same session.
362+
this._delegate.setModel(current);
363+
this._lastPushedSessionId = session.sessionId;
345364
}
346365
}
347366

src/vs/sessions/contrib/copilotChatSessions/test/browser/modelPickerDelegate.test.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import assert from 'assert';
7-
import { Event } from '../../../../../base/common/event.js';
7+
import { Emitter, Event } from '../../../../../base/common/event.js';
88
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
99
import { observableValue } from '../../../../../base/common/observable.js';
1010
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
@@ -33,7 +33,7 @@ function stubServices(
3333
storedEntries?: Map<string, string>;
3434
setModelSpy?: (sessionId: string, modelId: string) => void;
3535
},
36-
): { instantiationService: TestInstantiationService; storage: Map<string, string>; activeSession: ReturnType<typeof observableValue<IActiveSession | undefined>> } {
36+
): { instantiationService: TestInstantiationService; storage: Map<string, string>; activeSession: ReturnType<typeof observableValue<IActiveSession | undefined>>; fireLanguageModelsChanged: () => void } {
3737
const instantiationService = disposables.add(new TestInstantiationService());
3838
const models = opts?.models ?? [];
3939
const storage = opts?.storedEntries ?? new Map<string, string>();
@@ -44,8 +44,10 @@ function stubServices(
4444

4545
const setModelSpy = opts?.setModelSpy ?? (() => { });
4646

47+
const onDidChangeLanguageModelsEmitter = disposables.add(new Emitter<{ added?: readonly { identifier: string }[]; removed?: readonly string[] }>());
48+
4749
instantiationService.stub(ILanguageModelsService, {
48-
onDidChangeLanguageModels: Event.None,
50+
onDidChangeLanguageModels: onDidChangeLanguageModelsEmitter.event,
4951
getLanguageModelIds: () => models.map(m => m.identifier),
5052
lookupLanguageModel: (id: string) => models.find(m => m.identifier === id)?.metadata,
5153
} as Partial<ILanguageModelsService>);
@@ -72,7 +74,7 @@ function stubServices(
7274
// Stub IInstantiationService so SessionModelPicker can call createInstance for ModelPickerActionItem
7375
instantiationService.stub(IInstantiationService, instantiationService);
7476

75-
return { instantiationService, storage, activeSession };
77+
return { instantiationService, storage, activeSession, fireLanguageModelsChanged: () => onDidChangeLanguageModelsEmitter.fire({}) };
7678
}
7779

7880
suite('modelPickerStorageKey', () => {
@@ -185,4 +187,45 @@ suite('SessionModelPicker', () => {
185187
// CLI key should still be intact
186188
assert.strictEqual(storage.get(modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE)), 'cli-m');
187189
});
190+
191+
test('propagates selected model to a new session of the same type (#313385)', () => {
192+
const models = [makeModel('cli-a', COPILOT_CLI_SESSION_TYPE), makeModel('cli-b', COPILOT_CLI_SESSION_TYPE)];
193+
const storedEntries = new Map([[modelPickerStorageKey(COPILOT_CLI_SESSION_TYPE), 'cli-b']]);
194+
const calls: { sessionId: string; modelId: string }[] = [];
195+
const { instantiationService, activeSession } = stubServices(disposables, {
196+
models,
197+
activeSession: { providerId: 'default-copilot', sessionId: 's1', sessionType: COPILOT_CLI_SESSION_TYPE },
198+
storedEntries,
199+
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
200+
});
201+
disposables.add(instantiationService.createInstance(SessionModelPicker));
202+
// Initial session receives the remembered model.
203+
assert.ok(calls.some(c => c.sessionId === 's1' && c.modelId === 'cli-b'));
204+
205+
// Switch to a new session of the same type (e.g. user picked a different repo).
206+
activeSession.set({ providerId: 'default-copilot', sessionId: 's2', sessionType: COPILOT_CLI_SESSION_TYPE } as IActiveSession, undefined);
207+
208+
// The new session must receive the same model so the request isn't sent with the default.
209+
assert.ok(calls.some(c => c.sessionId === 's2' && c.modelId === 'cli-b'));
210+
});
211+
212+
test('does not re-push model to the same session when language models change', () => {
213+
const models = [makeModel('cli-a', COPILOT_CLI_SESSION_TYPE)];
214+
const calls: { sessionId: string; modelId: string }[] = [];
215+
const { instantiationService, fireLanguageModelsChanged } = stubServices(disposables, {
216+
models,
217+
activeSession: { providerId: 'default-copilot', sessionId: 's1', sessionType: COPILOT_CLI_SESSION_TYPE },
218+
setModelSpy: (sessionId, modelId) => calls.push({ sessionId, modelId }),
219+
});
220+
disposables.add(instantiationService.createInstance(SessionModelPicker));
221+
const initialCallCount = calls.filter(c => c.sessionId === 's1').length;
222+
assert.ok(initialCallCount > 0, 'expected initial setModel to fire');
223+
224+
// Re-fire language-models-changed multiple times. The active session and
225+
// selected model haven't changed, so the provider must not be re-notified.
226+
fireLanguageModelsChanged();
227+
fireLanguageModelsChanged();
228+
229+
assert.strictEqual(calls.filter(c => c.sessionId === 's1').length, initialCallCount);
230+
});
188231
});

0 commit comments

Comments
 (0)