Skip to content

Commit f6f5cf2

Browse files
authored
Add missing completions model picker and fix tests (#1811)
* add missing completions model picker * fix and run tests * add config in code
1 parent ed93e7d commit f6f5cf2

File tree

11 files changed

+318
-20
lines changed

11 files changed

+318
-20
lines changed

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2308,6 +2308,12 @@
23082308
"title": "Toggle (Enable/Disable) Inline Suggestions",
23092309
"enablement": "github.copilot.extensionUnification.activated && github.copilot.activated",
23102310
"category": "GitHub Copilot"
2311+
},
2312+
{
2313+
"command": "github.copilot.chat.openModelPicker",
2314+
"title": "Change Completions Model",
2315+
"category": "GitHub Copilot",
2316+
"enablement": "github.copilot.extensionUnification.activated && !isWeb"
23112317
}
23122318
],
23132319
"configuration": [
@@ -2455,6 +2461,11 @@
24552461
"type": "boolean"
24562462
},
24572463
"markdownDescription": "Enable or disable auto triggering of Copilot completions for specified [languages](https://code.visualstudio.com/docs/languages/identifiers). You can still trigger suggestions manually using `Alt + \\`"
2464+
},
2465+
"github.copilot.selectedCompletionModel": {
2466+
"type": "string",
2467+
"default": "",
2468+
"markdownDescription": "The currently selected completion model ID. To select from a list of available models, use the __\"Change Completions Model\"__ command or open the model picker (from the Copilot menu in the VS Code title bar, select __\"Configure Code Completions\"__ then __\"Change Completions Model\"__. The value must be a valid model ID. An empty value indicates that the default model will be used."
24582469
}
24592470
}
24602471
},
@@ -4607,4 +4618,4 @@
46074618
"string_decoder": "npm:string_decoder@1.2.0",
46084619
"node-gyp": "npm:node-gyp@10.3.1"
46094620
}
4610-
}
4621+
}

src/extension/completions-core/vscode-node/completionsServiceBridges.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@ import { CompletionsTelemetryServiceBridge, ICompletionsTelemetryService } from
2525
import { CodeReference } from './extension/src/codeReferencing';
2626
import { LoggingCitationManager } from './extension/src/codeReferencing/citationManager';
2727
import { disableCompletions, enableCompletions, toggleCompletions, VSCodeConfigProvider, VSCodeEditorInfo } from './extension/src/config';
28-
import { CMDDisableCompletionsChat, CMDDisableCompletionsClient, CMDEnableCompletionsChat, CMDEnableCompletionsClient, CMDOpenDocumentationClient, CMDOpenLogsClient, CMDToggleCompletionsChat, CMDToggleCompletionsClient, CMDToggleStatusMenuChat, CMDToggleStatusMenuClient } from './extension/src/constants';
28+
import { CMDDisableCompletionsChat, CMDDisableCompletionsClient, CMDEnableCompletionsChat, CMDEnableCompletionsClient, CMDOpenDocumentationClient, CMDOpenLogsClient, CMDOpenModelPickerChat, CMDOpenModelPickerClient, CMDToggleCompletionsChat, CMDToggleCompletionsClient, CMDToggleStatusMenuChat, CMDToggleStatusMenuClient } from './extension/src/constants';
2929
import { contextProviderMatch } from './extension/src/contextProviderMatch';
3030
import { registerPanelSupport } from './extension/src/copilotPanel/common';
3131
import { Extension } from './extension/src/extensionContext';
3232
import { CopilotExtensionStatus } from './extension/src/extensionStatus';
3333
import { extensionFileSystem } from './extension/src/fileSystem';
3434
import { registerGhostTextDependencies } from './extension/src/ghostText/ghostText';
3535
import { exception } from './extension/src/inlineCompletion';
36+
import { ModelPickerManager } from './extension/src/modelPicker';
3637
import { CopilotStatusBar } from './extension/src/statusBar';
3738
import { CopilotStatusBarPickMenu } from './extension/src/statusBarPicker';
3839
import { ExtensionTextDocumentManager } from './extension/src/textDocumentManager';
@@ -239,6 +240,7 @@ export function registerUnificationCommands(accessor: ServicesAccessor): IDispos
239240
disposables.add(registerStatusBar(accessor));
240241
disposables.add(registerDiagnosticCommands(accessor));
241242
disposables.add(registerPanelSupport(accessor));
243+
disposables.add(registerModelPickerCommands(accessor));
242244

243245
return disposables;
244246
}
@@ -277,6 +279,26 @@ function registerEnablementCommands(accessor: ServicesAccessor): IDisposable {
277279
return disposables;
278280
}
279281

282+
function registerModelPickerCommands(accessor: ServicesAccessor): IDisposable {
283+
const disposables = new DisposableStore();
284+
285+
const instantiationService = accessor.get(IInstantiationService);
286+
287+
const modelsPicker = instantiationService.createInstance(ModelPickerManager);
288+
289+
function registerModelPicker(commandId: string): IDisposable {
290+
return registerCommandWrapper(accessor, commandId, async () => {
291+
await modelsPicker.showModelPicker();
292+
});
293+
}
294+
295+
// Model picker command [with Command Palette support]
296+
disposables.add(registerModelPicker(CMDOpenModelPickerClient));
297+
disposables.add(registerModelPicker(CMDOpenModelPickerChat));
298+
299+
return disposables;
300+
}
301+
280302
function registerStatusBar(accessor: ServicesAccessor): IDisposable {
281303
const disposables = new DisposableStore();
282304

src/extension/completions-core/vscode-node/extension/src/codeReferencing/test/codeReferencing.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as Sinon from 'sinon';
77
import { Disposable, ExtensionContext } from 'vscode';
88
import { CodeReference } from '..';
99
import { CopilotToken } from '../../../../../../../platform/authentication/common/copilotToken';
10+
import { generateUuid } from '../../../../../../../util/vs/base/common/uuid';
1011
import { IInstantiationService } from '../../../../../../../util/vs/platform/instantiation/common/instantiation';
1112
import { ConnectionState } from '../../../../lib/src/snippy/connectionState';
1213
import { createExtensionTestingContext } from '../../test/context';
@@ -45,8 +46,8 @@ suite('CodeReference', function () {
4546

4647
test('should be updated correctly when token change events received', function () {
4748
const codeQuote = instantiationService.createInstance(CodeReference);
48-
const enabledToken = new CopilotToken({ token: this._completionsToken, expires_at: 0, refresh_in: 0, username: 'fixedTokenManager', isVscodeTeamMember: false, copilot_plan: 'unknown', code_quote_enabled: true });
49-
const disabledToken = new CopilotToken({ token: this._completionsToken, expires_at: 0, refresh_in: 0, username: 'fixedTokenManager', isVscodeTeamMember: false, copilot_plan: 'unknown', code_quote_enabled: false });
49+
const enabledToken = new CopilotToken({ token: `test token ${generateUuid()}`, expires_at: 0, refresh_in: 0, username: 'fixedTokenManager', isVscodeTeamMember: false, copilot_plan: 'unknown', code_quote_enabled: true });
50+
const disabledToken = new CopilotToken({ token: `test token ${generateUuid()}`, expires_at: 0, refresh_in: 0, username: 'fixedTokenManager', isVscodeTeamMember: false, copilot_plan: 'unknown', code_quote_enabled: false });
5051

5152
codeQuote.onCopilotToken(enabledToken);
5253

src/extension/completions-core/vscode-node/extension/src/codeReferencing/test/matchNotifier.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { Extension } from '../../extensionContext';
1414
import { createExtensionTestingContext } from '../../test/context';
1515
import { notify } from '../matchNotifier';
1616
import { IVSCodeExtensionContext } from '../../../../../../../platform/extContext/common/extensionContext';
17-
import { ServicesAccessor } from '../../../../../../../util/vs/platform/instantiation/common/instantiation';
17+
import { IInstantiationService, ServicesAccessor } from '../../../../../../../util/vs/platform/instantiation/common/instantiation';
1818

1919
/**
2020
* Minimal fake implementation of the VS Code globalState object.
@@ -44,7 +44,7 @@ suite('.match', function () {
4444
new Extension({
4545
extensionMode: ExtensionMode.Test,
4646
subscriptions: [] as { dispose(): void }[],
47-
extension: { id: 'copilot.extension-test' },
47+
extension: { id: 'copilot.extension-tfest' },
4848
globalState: new FakeGlobalState(),
4949
} as unknown as IVSCodeExtensionContext)
5050
);
@@ -139,14 +139,15 @@ suite('.match', function () {
139139

140140
test('does not notify if already notified', async function () {
141141
const ctx = accessor.get(ICompletionsContextService);
142-
const extensionContext = ctx.get(Extension);
143-
const globalState = extensionContext.context.globalState;
142+
const extensionContext = accessor.get(IVSCodeExtensionContext);
143+
const instantiationService = accessor.get(IInstantiationService);
144+
const globalState = extensionContext.globalState;
144145
const testNotificationSender = ctx.get(NotificationSender) as TestNotificationSender;
145146
testNotificationSender.performAction('View reference');
146147

147148
await globalState.update('codeReference.notified', true);
148149

149-
await notify(accessor);
150+
await instantiationService.invokeFunction(notify);
150151

151152
await testNotificationSender.waitForMessages();
152153

src/extension/completions-core/vscode-node/extension/src/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ export const CMDCollectDiagnosticsChat = 'github.copilot.debug.collectDiagnostic
3838

3939
// Context variable that enable/disable panel-specific commands
4040
export const CopilotPanelVisible = 'github.copilot.panelVisible';
41-
export const ComparisonPanelVisible = 'github.copilot.comparisonPanelVisible';
41+
export const ComparisonPanelVisible = 'github.copilot.comparisonPanelVisible';
42+
43+
export const CMDOpenModelPickerClient = 'github.copilot.openModelPicker';
44+
export const CMDOpenModelPickerChat = 'github.copilot.chat.openModelPicker';
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { env, QuickPick, QuickPickItem, QuickPickItemKind, Uri, window, workspace } from 'vscode';
6+
import { IInstantiationService, ServicesAccessor } from '../../../../../util/vs/platform/instantiation/common/instantiation';
7+
import { ConfigKey, getConfig } from '../../lib/src/config';
8+
import { CopilotConfigPrefix } from '../../lib/src/constants';
9+
import { ICompletionsContextService } from '../../lib/src/context';
10+
import { AsyncCompletionManager } from '../../lib/src/ghostText/asyncCompletions';
11+
import { CompletionsCache } from '../../lib/src/ghostText/completionsCache';
12+
import { Logger, LogTarget } from '../../lib/src/logger';
13+
import { AvailableModelsManager, ModelItem } from '../../lib/src/openai/model';
14+
import { telemetry, TelemetryData } from '../../lib/src/telemetry';
15+
const logger = new Logger('modelPicker');
16+
17+
interface ModelPickerItem extends Omit<ModelItem, 'preview' | 'tokenizer'>, QuickPickItem {
18+
// Distinguish between items in the quick pick
19+
type: 'model' | 'separator' | 'learn-more';
20+
}
21+
22+
// Separator and learn-more links are always shown in the quick pick
23+
const defaultModelPickerItems: ModelPickerItem[] = [
24+
// Add separator after the models
25+
{
26+
label: '',
27+
kind: QuickPickItemKind.Separator,
28+
modelId: 'separator',
29+
type: 'separator' as const,
30+
alwaysShow: true,
31+
},
32+
// Add "Learn more" item at the end
33+
{
34+
modelId: 'learn-more',
35+
label: 'Learn more $(link-external)',
36+
description: '',
37+
alwaysShow: true,
38+
type: 'learn-more' as const,
39+
},
40+
];
41+
42+
export class ModelPickerManager {
43+
// URL for information about Copilot models
44+
private readonly MODELS_INFO_URL = 'https://aka.ms/CopilotCompletionsModelPickerLearnMore';
45+
46+
get models(): ModelItem[] {
47+
return this._ctx.get(AvailableModelsManager).getGenericCompletionModels();
48+
}
49+
50+
private getDefaultModelId(): string {
51+
return this._ctx.get(AvailableModelsManager).getDefaultModelId();
52+
}
53+
54+
constructor(
55+
@IInstantiationService private readonly _instantiationService: IInstantiationService,
56+
@ICompletionsContextService private readonly _ctx: ICompletionsContextService
57+
) { }
58+
59+
async setUserSelectedCompletionModel(modelId: string | null) {
60+
return workspace
61+
.getConfiguration(CopilotConfigPrefix)
62+
.update(ConfigKey.UserSelectedCompletionModel, modelId ?? '', true);
63+
}
64+
65+
async handleModelSelection(quickpickList: QuickPick<ModelPickerItem>) {
66+
const model = quickpickList.activeItems[0];
67+
if (model === undefined) {
68+
return;
69+
}
70+
quickpickList.hide();
71+
72+
// Open up the link
73+
if (model.type === 'learn-more') {
74+
await env.openExternal(Uri.parse(this.MODELS_INFO_URL));
75+
this._instantiationService.invokeFunction(telemetry, 'modelPicker.learnMoreClicked');
76+
return;
77+
}
78+
79+
await this.selectModel(model);
80+
}
81+
82+
async selectModel(model: ModelPickerItem) {
83+
const currentModel = this._instantiationService.invokeFunction(getUserSelectedModelConfiguration);
84+
85+
if (currentModel !== model.modelId) {
86+
this._ctx.get(CompletionsCache).clear();
87+
this._ctx.get(AsyncCompletionManager).clear();
88+
}
89+
90+
const modelSelection = model.modelId === this.getDefaultModelId() ? null : model.modelId;
91+
await this.setUserSelectedCompletionModel(modelSelection);
92+
const logTarget = this._ctx.get(LogTarget);
93+
if (modelSelection === null) {
94+
logger.info(logTarget, `User selected default model; setting null`);
95+
} else {
96+
logger.info(logTarget, `Selected model: ${model.modelId}`);
97+
}
98+
99+
this._instantiationService.invokeFunction(
100+
telemetry,
101+
'modelPicker.modelSelected',
102+
TelemetryData.createAndMarkAsIssued({
103+
engineName: modelSelection ?? 'default',
104+
})
105+
);
106+
}
107+
108+
private modelsForModelPicker(): [string | null, ModelPickerItem[]] {
109+
const currentModelSelection = this._instantiationService.invokeFunction(getUserSelectedModelConfiguration);
110+
const items: ModelPickerItem[] = this.models.map(model => {
111+
return {
112+
modelId: model.modelId,
113+
label: `${model.label}${model.preview ? ' (Preview)' : ''}`,
114+
description: `(${model.modelId})`,
115+
alwaysShow: model.modelId === this.getDefaultModelId(),
116+
type: 'model' as const,
117+
};
118+
});
119+
120+
return [currentModelSelection, items];
121+
}
122+
123+
showModelPicker(): QuickPick<ModelPickerItem> {
124+
const [currentModelSelection, items] = this.modelsForModelPicker();
125+
126+
const quickPick = window.createQuickPick<ModelPickerItem>();
127+
quickPick.title = 'Change Completions Model';
128+
quickPick.items = [...items, ...defaultModelPickerItems];
129+
quickPick.onDidAccept(() => this.handleModelSelection(quickPick));
130+
131+
const currentModelOrDefault = currentModelSelection ?? this.getDefaultModelId();
132+
133+
// set the currently selected model as active
134+
const selectedItem = quickPick.items.find(item => item.modelId === currentModelOrDefault);
135+
if (selectedItem) {
136+
quickPick.activeItems = [selectedItem];
137+
}
138+
139+
quickPick.show();
140+
return quickPick;
141+
}
142+
}
143+
144+
function getUserSelectedModelConfiguration(accessor: ServicesAccessor): string | null {
145+
const value = getConfig<string | null>(accessor, ConfigKey.UserSelectedCompletionModel);
146+
return typeof value === 'string' && value.length > 0 ? value : null;
147+
}

src/extension/completions-core/vscode-node/extension/src/statusBarPicker.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { isWeb } from '../../../../../util/vs/base/common/platform';
77
import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
88
import { ICompletionsContextService } from '../../lib/src/context';
99
import { isCompletionEnabled, isInlineSuggestEnabled } from './config';
10-
import { CMDCollectDiagnosticsChat, CMDDisableCompletionsChat, CMDEnableCompletionsChat, CMDOpenDocumentationClient, CMDOpenLogsClient, CMDOpenPanelClient } from './constants';
10+
import { CMDCollectDiagnosticsChat, CMDDisableCompletionsChat, CMDEnableCompletionsChat, CMDOpenDocumentationClient, CMDOpenLogsClient, CMDOpenModelPickerClient, CMDOpenPanelClient } from './constants';
1111
import { CopilotExtensionStatus } from './extensionStatus';
1212
import { Icon } from './icon';
1313

@@ -66,7 +66,7 @@ export class CopilotStatusBarPickMenu {
6666
const editor = window.activeTextEditor;
6767
if (!isWeb && editor) { items.push(this.newPanelItem()); }
6868
// Always show the model picker even if only one model is available
69-
//if (!isWeb) { items.push(this.newChangeModelItem()); }
69+
if (!isWeb) { items.push(this.newChangeModelItem()); }
7070
if (editor) { items.push(...this.newEnableLanguageItem()); }
7171
if (items.length) { items.push(this.newSeparator()); }
7272

@@ -140,15 +140,11 @@ export class CopilotStatusBarPickMenu {
140140
private newPanelItem() {
141141
return this.newCommandItem('Open Completions Panel...', CMDOpenPanelClient);
142142
}
143-
/*
143+
144144
private newChangeModelItem() {
145-
return this.newCommandItem('Change Completions Model...', CMDOpenModelPicker);
145+
return this.newCommandItem('Change Completions Model...', CMDOpenModelPickerClient);
146146
}
147147

148-
private newForumItem() {
149-
return this.newCommandItem('$(comments-view-icon) View Copilot Forum...', CMDSendFeedback);
150-
} */
151-
152148
private newDocsItem() {
153149
return this.newCommandItem(
154150
'$(remote-explorer-documentation) View Copilot Documentation...',

0 commit comments

Comments
 (0)