Skip to content

Commit 69dbf1e

Browse files
authored
support chat agent name service (microsoft#210550)
* support chat agent name service * use regular object, render the same way
1 parent 063d715 commit 69dbf1e

File tree

4 files changed

+125
-6
lines changed

4 files changed

+125
-6
lines changed

src/vs/base/common/product.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export interface IProductConfiguration {
190190
readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust;
191191
readonly gitHubEntitlement?: IGitHubEntitlement;
192192
readonly chatWelcomeView?: IChatWelcomeView;
193+
readonly chatParticipantRegistry?: string;
193194
}
194195

195196
export interface ITunnelApplicationConfig {

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget'
4242
import { ChatCodeBlockContextProviderService } from 'vs/workbench/contrib/chat/browser/codeBlockContextProviderService';
4343
import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables';
4444
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
45-
import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
45+
import { ChatAgentLocation, ChatAgentService, IChatAgentService, IChatAgentNameService, ChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents';
4646
import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys';
4747
import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel';
4848
import { chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
@@ -342,6 +342,7 @@ registerSingleton(IChatWidgetHistoryService, ChatWidgetHistoryService, Instantia
342342
registerSingleton(ILanguageModelsService, LanguageModelsService, InstantiationType.Delayed);
343343
registerSingleton(IChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed);
344344
registerSingleton(IChatAgentService, ChatAgentService, InstantiationType.Delayed);
345+
registerSingleton(IChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed);
345346
registerSingleton(IChatVariablesService, ChatVariablesService, InstantiationType.Delayed);
346347
registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed);
347348
registerSingleton(IChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed);

src/vs/workbench/contrib/chat/browser/chatListRenderer.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups';
5454
import { ChatMarkdownDecorationsRenderer } from 'vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer';
5555
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
5656
import { ChatCodeBlockContentProvider, CodeBlockPart, CodeCompareBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
57-
import { ChatAgentLocation, IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents';
57+
import { ChatAgentLocation, IChatAgentMetadata, IChatAgentNameService } from 'vs/workbench/contrib/chat/common/chatAgents';
5858
import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
5959
import { IChatProgressRenderableResponseContent } from 'vs/workbench/contrib/chat/common/chatModel';
6060
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
@@ -71,6 +71,8 @@ import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/text
7171
import { TextEdit } from 'vs/editor/common/languages';
7272
import { IChatListItemRendererOptions } from './chat';
7373
import { CancellationTokenSource } from 'vs/base/common/cancellation';
74+
import { autorun, constObservable, IObservable } from 'vs/base/common/observable';
75+
import { isUndefined } from 'vs/base/common/types';
7476

7577
const $ = dom.$;
7678

@@ -146,6 +148,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
146148
@ICommandService private readonly commandService: ICommandService,
147149
@ITextModelService private readonly textModelService: ITextModelService,
148150
@IModelService private readonly modelService: IModelService,
151+
@IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService,
149152
) {
150153
super();
151154

@@ -367,9 +370,23 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
367370
}
368371

369372
private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void {
370-
let progressMsg: string = '';
373+
let agentName: IObservable<string | undefined> = constObservable(undefined);
374+
371375
if (element.agent && !element.agent.isDefault) {
372-
let usingMsg = chatAgentLeader + element.agent.name;
376+
const name = element.agent.name;
377+
agentName = this.chatAgentNameService.getAgentNameRestriction(element.agent)
378+
.map(allowed => allowed ? name : name); // TODO
379+
}
380+
381+
templateData.elementDisposables.add(autorun(reader => {
382+
this._renderDetail(element, agentName.read(reader), templateData);
383+
}));
384+
}
385+
386+
private _renderDetail(element: IChatResponseViewModel, agentName: string | undefined, templateData: IChatListItemTemplate): void {
387+
let progressMsg: string = '';
388+
if (!isUndefined(agentName)) {
389+
let usingMsg = chatAgentLeader + agentName;
373390
if (element.slashCommand) {
374391
usingMsg += ` ${chatSubcommandLeader}${element.slashCommand.name}`;
375392
}
@@ -381,8 +398,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
381398
}
382399
} else if (element.agentOrSlashCommandDetected) {
383400
const usingMsg: string[] = [];
384-
if (element.agent && !element.agent.isDefault) {
385-
usingMsg.push(chatAgentLeader + element.agent.name);
401+
if (!isUndefined(agentName)) {
402+
usingMsg.push(chatAgentLeader + agentName);
386403
}
387404
if (element.slashCommand) {
388405
usingMsg.push(chatSubcommandLeader + element.slashCommand.name);

src/vs/workbench/contrib/chat/common/chatAgents.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,25 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { timeout } from 'vs/base/common/async';
67
import { CancellationToken } from 'vs/base/common/cancellation';
78
import { Emitter, Event } from 'vs/base/common/event';
89
import { IMarkdownString } from 'vs/base/common/htmlContent';
910
import { Iterable } from 'vs/base/common/iterator';
1011
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
12+
import { IObservable } from 'vs/base/common/observable';
13+
import { observableValue } from 'vs/base/common/observableInternal/base';
14+
import { equalsIgnoreCase } from 'vs/base/common/strings';
1115
import { ThemeIcon } from 'vs/base/common/themables';
1216
import { URI } from 'vs/base/common/uri';
1317
import { ProviderResult } from 'vs/editor/common/languages';
1418
import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1519
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
1620
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
21+
import { ILogService } from 'vs/platform/log/common/log';
22+
import { IProductService } from 'vs/platform/product/common/productService';
23+
import { asJson, IRequestService } from 'vs/platform/request/common/request';
24+
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
1725
import { CONTEXT_CHAT_ENABLED } from 'vs/workbench/contrib/chat/common/chatContextKeys';
1826
import { IChatProgressResponseContent, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel';
1927
import { IRawChatCommandContribution, RawChatParticipantLocation } from 'vs/workbench/contrib/chat/common/chatParticipantContribTypes';
@@ -351,3 +359,95 @@ export class MergedChatAgent implements IChatAgent {
351359
return undefined;
352360
}
353361
}
362+
363+
export const IChatAgentNameService = createDecorator<IChatAgentNameService>('chatAgentNameService');
364+
365+
type IChatParticipantRegistry = { [name: string]: string[] };
366+
367+
interface IChatParticipantRegistryResponse {
368+
readonly version: number;
369+
readonly restrictedChatParticipants: IChatParticipantRegistry;
370+
}
371+
372+
export interface IChatAgentNameService {
373+
_serviceBrand: undefined;
374+
getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable<boolean>;
375+
}
376+
377+
export class ChatAgentNameService implements IChatAgentNameService {
378+
379+
private static readonly StorageKey = 'chat.participantNameRegistry';
380+
381+
declare _serviceBrand: undefined;
382+
383+
private readonly url!: string;
384+
private registry = observableValue<IChatParticipantRegistry>(this, Object.create(null));
385+
private disposed = false;
386+
387+
constructor(
388+
@IProductService productService: IProductService,
389+
@IRequestService private readonly requestService: IRequestService,
390+
@ILogService private readonly logService: ILogService,
391+
@IStorageService private readonly storageService: IStorageService
392+
) {
393+
if (!productService.chatParticipantRegistry) {
394+
return;
395+
}
396+
397+
this.url = productService.chatParticipantRegistry;
398+
399+
const raw = storageService.get(ChatAgentNameService.StorageKey, StorageScope.APPLICATION);
400+
401+
try {
402+
this.registry.set(JSON.parse(raw ?? '{}'), undefined);
403+
} catch (err) {
404+
storageService.remove(ChatAgentNameService.StorageKey, StorageScope.APPLICATION);
405+
}
406+
407+
this.refresh();
408+
}
409+
410+
private refresh(): void {
411+
if (this.disposed) {
412+
return;
413+
}
414+
415+
this.update()
416+
.catch(err => this.logService.warn('Failed to fetch chat participant registry', err))
417+
.then(() => timeout(5 * 60 * 1000)) // every 5 minutes
418+
.then(() => this.refresh());
419+
}
420+
421+
private async update(): Promise<void> {
422+
const context = await this.requestService.request({ type: 'GET', url: this.url }, CancellationToken.None);
423+
424+
if (context.res.statusCode !== 200) {
425+
throw new Error('Could not get extensions report.');
426+
}
427+
428+
const result = await asJson<IChatParticipantRegistryResponse>(context);
429+
430+
if (!result || result.version !== 1) {
431+
throw new Error('Unexpected chat participant registry response.');
432+
}
433+
434+
const registry = result.restrictedChatParticipants;
435+
this.registry.set(registry, undefined);
436+
this.storageService.store(ChatAgentNameService.StorageKey, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE);
437+
}
438+
439+
getAgentNameRestriction(chatAgentData: IChatAgentData): IObservable<boolean> {
440+
const allowList = this.registry.map<string[] | undefined>(registry => registry[chatAgentData.name.toLowerCase()]);
441+
return allowList.map(allowList => {
442+
if (!allowList) {
443+
return true;
444+
}
445+
446+
return allowList.some(id => equalsIgnoreCase(id, id.includes('.') ? chatAgentData.extensionId.value : chatAgentData.extensionPublisher));
447+
});
448+
}
449+
450+
dispose() {
451+
this.disposed = true;
452+
}
453+
}

0 commit comments

Comments
 (0)