Skip to content

Commit 1c7035b

Browse files
authored
improvements to models management editor (#278653)
1 parent 33ea003 commit 1c7035b

File tree

3 files changed

+171
-148
lines changed

3 files changed

+171
-148
lines changed

src/vs/workbench/contrib/chat/browser/chatManagement/chatModelsViewModel.ts

Lines changed: 134 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const VENDOR_ENTRY_TEMPLATE_ID = 'vendor.entry.template';
1616
const wordFilter = or(matchesBaseContiguousSubString, matchesWords);
1717
const CAPABILITY_REGEX = /@capability:\s*([^\s]+)/gi;
1818
const VISIBLE_REGEX = /@visible:\s*(true|false)/i;
19+
const PROVIDER_REGEX = /@provider:\s*((".+?")|([^\s]+))/gi;
1920

2021
export const SEARCH_SUGGESTIONS = {
2122
FILTER_TYPES: [
@@ -54,6 +55,7 @@ export interface IModelItemEntry {
5455
templateId: string;
5556
providerMatches?: IMatch[];
5657
modelNameMatches?: IMatch[];
58+
modelIdMatches?: IMatch[];
5759
capabilityMatches?: string[];
5860
}
5961

@@ -111,92 +113,154 @@ export class ChatModelsViewModel extends EditorModel {
111113

112114
filter(searchValue: string): readonly IViewModelEntry[] {
113115
this.searchValue = searchValue;
116+
const filtered = this.filterModels(this.modelEntries, searchValue);
117+
this.splice(0, this._viewModelEntries.length, filtered);
118+
return this.viewModelEntries;
119+
}
114120

115-
let modelEntries = this.modelEntries;
116-
const capabilityMatchesMap = new Map<string, string[]>();
121+
private filterModels(modelEntries: IModelEntry[], searchValue: string): (IVendorItemEntry | IModelItemEntry)[] {
122+
let visible: boolean | undefined;
117123

118124
const visibleMatches = VISIBLE_REGEX.exec(searchValue);
119125
if (visibleMatches && visibleMatches[1]) {
120-
const visible = visibleMatches[1].toLowerCase() === 'true';
121-
modelEntries = this.filterByVisible(modelEntries, visible);
126+
visible = visibleMatches[1].toLowerCase() === 'true';
122127
searchValue = searchValue.replace(VISIBLE_REGEX, '');
123128
}
124129

125130
const providerNames: string[] = [];
126-
let match: RegExpExecArray | null;
127-
128-
const providerRegexGlobal = /@provider:\s*((".+?")|([^\s]+))/gi;
129-
while ((match = providerRegexGlobal.exec(searchValue)) !== null) {
130-
const providerName = match[2] ? match[2].substring(1, match[2].length - 1) : match[3];
131+
let providerMatch: RegExpExecArray | null;
132+
PROVIDER_REGEX.lastIndex = 0;
133+
while ((providerMatch = PROVIDER_REGEX.exec(searchValue)) !== null) {
134+
const providerName = providerMatch[2] ? providerMatch[2].substring(1, providerMatch[2].length - 1) : providerMatch[3];
131135
providerNames.push(providerName);
132136
}
133-
134-
// Apply provider filter with OR logic if multiple providers
135137
if (providerNames.length > 0) {
136-
modelEntries = this.filterByProviders(modelEntries, providerNames);
137-
searchValue = searchValue.replace(/@provider:\s*((".+?")|([^\s]+))/gi, '').replace(/@vendor:\s*((".+?")|([^\s]+))/gi, '');
138+
searchValue = searchValue.replace(PROVIDER_REGEX, '');
138139
}
139140

140-
// Apply capability filters with AND logic if multiple capabilities
141-
const capabilityNames: string[] = [];
141+
const capabilities: string[] = [];
142142
let capabilityMatch: RegExpExecArray | null;
143-
143+
CAPABILITY_REGEX.lastIndex = 0;
144144
while ((capabilityMatch = CAPABILITY_REGEX.exec(searchValue)) !== null) {
145-
capabilityNames.push(capabilityMatch[1].toLowerCase());
145+
capabilities.push(capabilityMatch[1].toLowerCase());
146146
}
147-
148-
if (capabilityNames.length > 0) {
149-
const filteredEntries = this.filterByCapabilities(modelEntries, capabilityNames);
150-
modelEntries = [];
151-
for (const { entry, matchedCapabilities } of filteredEntries) {
152-
modelEntries.push(entry);
153-
capabilityMatchesMap.set(ChatModelsViewModel.getId(entry), matchedCapabilities);
154-
}
155-
searchValue = searchValue.replace(/@capability:\s*([^\s]+)/gi, '');
147+
if (capabilities.length > 0) {
148+
searchValue = searchValue.replace(CAPABILITY_REGEX, '');
156149
}
157150

151+
const quoteAtFirstChar = searchValue.charAt(0) === '"';
152+
const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"';
153+
const completeMatch = quoteAtFirstChar && quoteAtLastChar;
154+
if (quoteAtFirstChar) {
155+
searchValue = searchValue.substring(1);
156+
}
157+
if (quoteAtLastChar) {
158+
searchValue = searchValue.substring(0, searchValue.length - 1);
159+
}
158160
searchValue = searchValue.trim();
159-
const filtered = searchValue ? this.filterByText(modelEntries, searchValue, capabilityMatchesMap) : this.toEntries(modelEntries, capabilityMatchesMap);
160161

161-
this.splice(0, this._viewModelEntries.length, filtered);
162-
return this.viewModelEntries;
163-
}
162+
const isFiltering = searchValue !== '' || capabilities.length > 0 || providerNames.length > 0 || visible !== undefined;
164163

165-
private filterByProviders(modelEntries: IModelEntry[], providers: string[]): IModelEntry[] {
166-
const lowerProviders = providers.map(p => p.toLowerCase().trim());
167-
return modelEntries.filter(m =>
168-
lowerProviders.some(provider =>
169-
m.vendor.toLowerCase() === provider ||
170-
m.vendorDisplayName.toLowerCase() === provider
171-
)
172-
);
173-
}
174-
175-
private filterByVisible(modelEntries: IModelEntry[], visible: boolean): IModelEntry[] {
176-
return modelEntries.filter(m => (m.metadata.isUserSelectable ?? false) === visible);
177-
}
164+
const result: (IVendorItemEntry | IModelItemEntry)[] = [];
165+
const words = searchValue.split(' ');
166+
const allVendors = new Set(this.modelEntries.map(m => m.vendor));
167+
const showHeaders = allVendors.size > 1;
168+
const addedVendors = new Set<string>();
169+
const lowerProviders = providerNames.map(p => p.toLowerCase().trim());
178170

179-
private filterByCapabilities(modelEntries: IModelEntry[], capabilities: string[]): { entry: IModelEntry; matchedCapabilities: string[] }[] {
180-
const result: { entry: IModelEntry; matchedCapabilities: string[] }[] = [];
181-
for (const m of modelEntries) {
182-
if (!m.metadata.capabilities) {
171+
for (const modelEntry of modelEntries) {
172+
if (!isFiltering && showHeaders && this.collapsedVendors.has(modelEntry.vendor)) {
173+
if (!addedVendors.has(modelEntry.vendor)) {
174+
const vendorInfo = this.languageModelsService.getVendors().find(v => v.vendor === modelEntry.vendor);
175+
result.push({
176+
type: 'vendor',
177+
id: `vendor-${modelEntry.vendor}`,
178+
vendorEntry: {
179+
vendor: modelEntry.vendor,
180+
vendorDisplayName: modelEntry.vendorDisplayName,
181+
managementCommand: vendorInfo?.managementCommand
182+
},
183+
templateId: VENDOR_ENTRY_TEMPLATE_ID,
184+
collapsed: true
185+
});
186+
addedVendors.add(modelEntry.vendor);
187+
}
183188
continue;
184189
}
185-
const allMatchedCapabilities: string[] = [];
186-
let matchesAll = true;
187-
188-
for (const capability of capabilities) {
189-
const matchedForThisCapability = this.getMatchingCapabilities(m, capability);
190-
if (matchedForThisCapability.length === 0) {
191-
matchesAll = false;
192-
break;
190+
191+
if (visible !== undefined) {
192+
if ((modelEntry.metadata.isUserSelectable ?? false) !== visible) {
193+
continue;
194+
}
195+
}
196+
197+
if (lowerProviders.length > 0) {
198+
const matchesProvider = lowerProviders.some(provider =>
199+
modelEntry.vendor.toLowerCase() === provider ||
200+
modelEntry.vendorDisplayName.toLowerCase() === provider
201+
);
202+
if (!matchesProvider) {
203+
continue;
193204
}
194-
allMatchedCapabilities.push(...matchedForThisCapability);
195205
}
196206

197-
if (matchesAll) {
198-
result.push({ entry: m, matchedCapabilities: distinct(allMatchedCapabilities) });
207+
// Filter by capabilities
208+
let matchedCapabilities: string[] = [];
209+
if (capabilities.length > 0) {
210+
if (!modelEntry.metadata.capabilities) {
211+
continue;
212+
}
213+
let matchesAll = true;
214+
for (const capability of capabilities) {
215+
const matchedForThisCapability = this.getMatchingCapabilities(modelEntry, capability);
216+
if (matchedForThisCapability.length === 0) {
217+
matchesAll = false;
218+
break;
219+
}
220+
matchedCapabilities.push(...matchedForThisCapability);
221+
}
222+
if (!matchesAll) {
223+
continue;
224+
}
225+
matchedCapabilities = distinct(matchedCapabilities);
226+
}
227+
228+
// Filter by text
229+
let modelMatches: ModelItemMatches | undefined;
230+
if (searchValue) {
231+
modelMatches = new ModelItemMatches(modelEntry, searchValue, words, completeMatch);
232+
if (!modelMatches.modelNameMatches && !modelMatches.modelIdMatches && !modelMatches.providerMatches && !modelMatches.capabilityMatches) {
233+
continue;
234+
}
235+
}
236+
237+
if (showHeaders && !addedVendors.has(modelEntry.vendor)) {
238+
const vendorInfo = this.languageModelsService.getVendors().find(v => v.vendor === modelEntry.vendor);
239+
result.push({
240+
type: 'vendor',
241+
id: `vendor-${modelEntry.vendor}`,
242+
vendorEntry: {
243+
vendor: modelEntry.vendor,
244+
vendorDisplayName: modelEntry.vendorDisplayName,
245+
managementCommand: vendorInfo?.managementCommand
246+
},
247+
templateId: VENDOR_ENTRY_TEMPLATE_ID,
248+
collapsed: false
249+
});
250+
addedVendors.add(modelEntry.vendor);
199251
}
252+
253+
const modelId = ChatModelsViewModel.getId(modelEntry);
254+
result.push({
255+
type: 'model',
256+
id: modelId,
257+
templateId: MODEL_ENTRY_TEMPLATE_ID,
258+
modelEntry,
259+
modelNameMatches: modelMatches?.modelNameMatches || undefined,
260+
modelIdMatches: modelMatches?.modelIdMatches || undefined,
261+
providerMatches: modelMatches?.providerMatches || undefined,
262+
capabilityMatches: matchedCapabilities.length ? matchedCapabilities : undefined,
263+
});
200264
}
201265
return result;
202266
}
@@ -239,42 +303,6 @@ export class ChatModelsViewModel extends EditorModel {
239303
return matchedCapabilities;
240304
}
241305

242-
private filterByText(modelEntries: IModelEntry[], searchValue: string, capabilityMatchesMap: Map<string, string[]>): IModelItemEntry[] {
243-
const quoteAtFirstChar = searchValue.charAt(0) === '"';
244-
const quoteAtLastChar = searchValue.charAt(searchValue.length - 1) === '"';
245-
const completeMatch = quoteAtFirstChar && quoteAtLastChar;
246-
if (quoteAtFirstChar) {
247-
searchValue = searchValue.substring(1);
248-
}
249-
if (quoteAtLastChar) {
250-
searchValue = searchValue.substring(0, searchValue.length - 1);
251-
}
252-
searchValue = searchValue.trim();
253-
254-
const result: IModelItemEntry[] = [];
255-
const words = searchValue.split(' ');
256-
257-
for (const modelEntry of modelEntries) {
258-
const modelMatches = new ModelItemMatches(modelEntry, searchValue, words, completeMatch);
259-
if (modelMatches.modelNameMatches
260-
|| modelMatches.providerMatches
261-
|| modelMatches.capabilityMatches
262-
) {
263-
const modelId = ChatModelsViewModel.getId(modelEntry);
264-
result.push({
265-
type: 'model',
266-
id: modelId,
267-
templateId: MODEL_ENTRY_TEMPLATE_ID,
268-
modelEntry,
269-
modelNameMatches: modelMatches.modelNameMatches || undefined,
270-
providerMatches: modelMatches.providerMatches || undefined,
271-
capabilityMatches: capabilityMatchesMap.get(modelId),
272-
});
273-
}
274-
}
275-
return result;
276-
}
277-
278306
getVendors(): IUserFriendlyLanguageModel[] {
279307
return [...this.languageModelsService.getVendors()].sort((a, b) => {
280308
if (a.vendor === 'copilot') { return -1; }
@@ -342,62 +370,28 @@ export class ChatModelsViewModel extends EditorModel {
342370
this.filter(this.searchValue);
343371
}
344372

345-
getConfiguredVendors(): IVendorItemEntry[] {
346-
return this.toEntries(this.modelEntries, new Map(), true) as IVendorItemEntry[];
347-
}
348-
349-
private toEntries(modelEntries: IModelEntry[], capabilityMatchesMap: Map<string, string[]>, excludeModels?: boolean): (IVendorItemEntry | IModelItemEntry)[] {
350-
const result: (IVendorItemEntry | IModelItemEntry)[] = [];
351-
const vendorMap = new Map<string, IModelEntry[]>();
352-
353-
for (const modelEntry of modelEntries) {
354-
const models = vendorMap.get(modelEntry.vendor) || [];
355-
models.push(modelEntry);
356-
vendorMap.set(modelEntry.vendor, models);
357-
}
358-
359-
const showVendorHeaders = vendorMap.size > 1;
360-
361-
for (const [vendor, models] of vendorMap) {
362-
const firstModel = models[0];
363-
const isCollapsed = this.collapsedVendors.has(vendor);
364-
const vendorInfo = this.languageModelsService.getVendors().find(v => v.vendor === vendor);
365-
366-
if (showVendorHeaders) {
373+
getConfiguredVendors(): IVendorEntry[] {
374+
const result: IVendorEntry[] = [];
375+
const seenVendors = new Set<string>();
376+
for (const modelEntry of this.modelEntries) {
377+
if (!seenVendors.has(modelEntry.vendor)) {
378+
seenVendors.add(modelEntry.vendor);
379+
const vendorInfo = this.languageModelsService.getVendors().find(v => v.vendor === modelEntry.vendor);
367380
result.push({
368-
type: 'vendor',
369-
id: `vendor-${vendor}`,
370-
vendorEntry: {
371-
vendor: firstModel.vendor,
372-
vendorDisplayName: firstModel.vendorDisplayName,
373-
managementCommand: vendorInfo?.managementCommand
374-
},
375-
templateId: VENDOR_ENTRY_TEMPLATE_ID,
376-
collapsed: isCollapsed
381+
vendor: modelEntry.vendor,
382+
vendorDisplayName: modelEntry.vendorDisplayName,
383+
managementCommand: vendorInfo?.managementCommand
377384
});
378385
}
379-
380-
if (!excludeModels && (!isCollapsed || !showVendorHeaders)) {
381-
for (const modelEntry of models) {
382-
const modelId = ChatModelsViewModel.getId(modelEntry);
383-
result.push({
384-
type: 'model',
385-
id: modelId,
386-
modelEntry,
387-
templateId: MODEL_ENTRY_TEMPLATE_ID,
388-
capabilityMatches: capabilityMatchesMap.get(modelId),
389-
});
390-
}
391-
}
392386
}
393-
394387
return result;
395388
}
396389
}
397390

398391
class ModelItemMatches {
399392

400393
readonly modelNameMatches: IMatch[] | null = null;
394+
readonly modelIdMatches: IMatch[] | null = null;
401395
readonly providerMatches: IMatch[] | null = null;
402396
readonly capabilityMatches: IMatch[] | null = null;
403397

@@ -408,10 +402,7 @@ class ModelItemMatches {
408402
this.matches(searchValue, modelEntry.metadata.name, (word, wordToMatchAgainst) => matchesWords(word, wordToMatchAgainst, true), words) :
409403
null;
410404

411-
// Match against model identifier
412-
if (!this.modelNameMatches) {
413-
this.modelNameMatches = this.matches(searchValue, modelEntry.identifier, or(matchesWords, matchesCamelCase), words);
414-
}
405+
this.modelIdMatches = this.matches(searchValue, modelEntry.identifier, or(matchesWords, matchesCamelCase), words);
415406

416407
// Match against vendor display name
417408
this.providerMatches = this.matches(searchValue, modelEntry.vendorDisplayName, (word, wordToMatchAgainst) => matchesWords(word, wordToMatchAgainst, true), words);

0 commit comments

Comments
 (0)