Merge pull request #272110 from microsoft/governing-barracuda

Proposed agent terminology
This commit is contained in:
Peng Lyu
2025-10-19 13:49:22 -07:00
committed by GitHub
7 changed files with 203 additions and 22 deletions
@@ -21,6 +21,8 @@ import { EditorInput, IEditorCloseHandler } from '../../../common/editor/editorI
import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';
import { IChatModel } from '../common/chatModel.js';
import { IChatService } from '../common/chatService.js';
import { IChatSessionsService } from '../common/chatSessionsService.js';
import { ChatSessionUri } from '../common/chatUri.js';
import { ChatAgentLocation, ChatEditorTitleMaxLength } from '../common/constants.js';
import { IClearEditingSessionConfirmationOptions } from './actions/chatActions.js';
import type { IChatEditorOptions } from './chatEditor.js';
@@ -39,6 +41,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
public sessionId: string | undefined;
private hasCustomTitle: boolean = false;
private cachedIcon: ThemeIcon | URI | undefined;
private model: IChatModel | undefined;
@@ -61,6 +64,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
readonly options: IChatEditorOptions,
@IChatService private readonly chatService: IChatService,
@IDialogService private readonly dialogService: IDialogService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
) {
super();
@@ -182,10 +186,59 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
return defaultName + inputCountSuffix;
}
override getIcon(): ThemeIcon {
override getIcon(): ThemeIcon | undefined {
// Return cached icon if available
if (this.cachedIcon) {
return ThemeIcon.isThemeIcon(this.cachedIcon) ? this.cachedIcon : undefined;
}
// Try to resolve icon and cache it
const resolvedIcon = this.resolveIcon();
if (resolvedIcon) {
this.cachedIcon = resolvedIcon;
return ThemeIcon.isThemeIcon(resolvedIcon) ? resolvedIcon : undefined;
}
// Fall back to default icon
return ChatEditorIcon;
}
private resolveIcon(): ThemeIcon | URI | undefined {
// TODO@osortega,@rebornix double check: Chat Session Item icon is reserved for chat session list and deprecated for chat session status. thus here we use session type icon. We may want to show status for the Editor Title.
const sessionType = this.getSessionType();
if (sessionType !== 'local') {
const typeIcon = this.chatSessionsService.getIconForSessionType(sessionType);
if (typeIcon) {
return typeIcon;
}
}
return undefined;
}
private getSessionType(): string {
if (!this.resource) {
return 'local';
}
const { scheme, query } = this.resource;
if (scheme === Schemas.vscodeChatSession) {
const parsed = ChatSessionUri.parse(this.resource);
if (parsed) {
return parsed.chatSessionType;
}
}
const sessionTypeFromQuery = new URLSearchParams(query).get('chatSessionType');
if (sessionTypeFromQuery) {
return sessionTypeFromQuery;
}
// Default to 'local' for vscode-chat-editor scheme or when type cannot be determined
return 'local';
}
override async resolve(): Promise<ChatEditorModel | null> {
const searchParams = new URLSearchParams(this.resource.query);
const chatSessionType = searchParams.get('chatSessionType');
@@ -215,12 +268,31 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
ChatEditorInput.countsInUseMap.delete(this.inputName);
}
}
// Invalidate icon cache when label changes
this.cachedIcon = undefined;
this._onDidChangeLabel.fire();
}));
// Check if icon has changed after model resolution
const newIcon = this.resolveIcon();
if (newIcon && (!this.cachedIcon || !this.iconsEqual(this.cachedIcon, newIcon))) {
this.cachedIcon = newIcon;
this._onDidChangeLabel.fire();
}
return this._register(new ChatEditorModel(this.model));
}
private iconsEqual(a: ThemeIcon | URI, b: ThemeIcon | URI): boolean {
if (ThemeIcon.isThemeIcon(a) && ThemeIcon.isThemeIcon(b)) {
return a.id === b.id;
}
if (a instanceof URI && b instanceof URI) {
return a.toString() === b.toString();
}
return false;
}
override dispose(): void {
super.dispose();
if (this.sessionId) {
@@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { URI } from '../../../../base/common/uri.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { localize, localize2 } from '../../../../nls.js';
@@ -57,6 +58,22 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsEx
description: localize('chatSessionsExtPoint.when', 'Condition which must be true to show this item.'),
type: 'string'
},
icon: {
description: localize('chatSessionsExtPoint.icon', 'Icon identifier (codicon ID) for the chat session editor tab. For example, "$(github)" or "$(cloud)".'),
type: 'string'
},
welcomeTitle: {
description: localize('chatSessionsExtPoint.welcomeTitle', 'Title text to display in the chat welcome view for this session type.'),
type: 'string'
},
welcomeMessage: {
description: localize('chatSessionsExtPoint.welcomeMessage', 'Message text (supports markdown) to display in the chat welcome view for this session type.'),
type: 'string'
},
inputPlaceholder: {
description: localize('chatSessionsExtPoint.inputPlaceholder', 'Placeholder text to display in the chat input box for this session type.'),
type: 'string'
},
capabilities: {
description: localize('chatSessionsExtPoint.capabilities', 'Optional capabilities for this chat session.'),
type: 'object',
@@ -160,6 +177,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; }
private readonly inProgressMap: Map<string, number> = new Map();
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
private readonly _sessionTypeIcons: Map<string, ThemeIcon> = new Map();
private readonly _sessionTypeWelcomeTitles: Map<string, string> = new Map();
private readonly _sessionTypeWelcomeMessages: Map<string, string> = new Map();
private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();
constructor(
@ILogService private readonly _logService: ILogService,
@@ -183,6 +204,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
displayName: contribution.displayName,
description: contribution.description,
when: contribution.when,
icon: contribution.icon,
welcomeTitle: contribution.welcomeTitle,
welcomeMessage: contribution.welcomeMessage,
inputPlaceholder: contribution.inputPlaceholder,
capabilities: contribution.capabilities,
extensionDescription: ext.description,
commands: contribution.commands
@@ -208,7 +233,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
let displayName: string | undefined;
if (chatSessionType === 'local') {
displayName = 'Local Chat Sessions';
displayName = 'Local Chat Agent';
} else {
displayName = this._contributions.get(chatSessionType)?.displayName;
}
@@ -250,11 +275,41 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
}
this._contributions.set(contribution.type, contribution);
// Store icon mapping if provided
let icon: ThemeIcon | undefined;
if (contribution.icon) {
// Parse icon string - support both "$(iconId)" and "iconId" formats
icon = contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')
? ThemeIcon.fromString(contribution.icon)
: ThemeIcon.fromId(contribution.icon);
}
if (icon) {
this._sessionTypeIcons.set(contribution.type, icon);
}
// Store welcome title, message, and input placeholder if provided
if (contribution.welcomeTitle) {
this._sessionTypeWelcomeTitles.set(contribution.type, contribution.welcomeTitle);
}
if (contribution.welcomeMessage) {
this._sessionTypeWelcomeMessages.set(contribution.type, contribution.welcomeMessage);
}
if (contribution.inputPlaceholder) {
this._sessionTypeInputPlaceholders.set(contribution.type, contribution.inputPlaceholder);
}
this._evaluateAvailability();
return {
dispose: () => {
this._contributions.delete(contribution.type);
this._sessionTypeIcons.delete(contribution.type);
this._sessionTypeWelcomeTitles.delete(contribution.type);
this._sessionTypeWelcomeMessages.delete(contribution.type);
this._sessionTypeInputPlaceholders.delete(contribution.type);
const store = this._disposableStores.get(contribution.type);
if (store) {
store.dispose();
@@ -276,7 +331,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
return MenuRegistry.appendMenuItem(MenuId.ViewTitle, {
command: {
id: `${NEW_CHAT_SESSION_ACTION_ID}.${contribution.type}`,
title: localize('interactiveSession.openNewSessionEditor', "New {0} Chat Editor", contribution.displayName),
title: localize('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),
icon: Codicon.plus,
source: {
id: contribution.extensionDescription.identifier.value,
@@ -296,7 +351,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
constructor() {
super({
id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,
title: localize2('interactiveSession.openNewSessionEditor', "New {0} Chat Editor", contribution.displayName),
title: localize2('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),
category: CHAT_CATEGORY,
icon: Codicon.plus,
f1: true, // Show in command palette
@@ -315,7 +370,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
override: ChatEditorInput.EditorID,
pinned: true,
title: {
fallback: localize('chatEditorContributionName', "{0} chat", contribution.displayName),
fallback: localize('chatEditorContributionName', "{0}", contribution.displayName),
}
};
const untitledId = `untitled-${generateUuid()}`;
@@ -660,6 +715,34 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
this.setSessionOption(chatSessionType, sessionId, u.optionId, u.value);
}
}
/**
* Get the icon for a specific session type
*/
public getIconForSessionType(chatSessionType: string): ThemeIcon | undefined {
return this._sessionTypeIcons.get(chatSessionType);
}
/**
* Get the welcome title for a specific session type
*/
public getWelcomeTitleForSessionType(chatSessionType: string): string | undefined {
return this._sessionTypeWelcomeTitles.get(chatSessionType);
}
/**
* Get the welcome message for a specific session type
*/
public getWelcomeMessageForSessionType(chatSessionType: string): string | undefined {
return this._sessionTypeWelcomeMessages.get(chatSessionType);
}
/**
* Get the input placeholder for a specific session type
*/
public getInputPlaceholderForSessionType(chatSessionType: string): string | undefined {
return this._sessionTypeInputPlaceholders.get(chatSessionType);
}
}
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
@@ -254,7 +254,7 @@ class ChatSessionsViewPaneContainer extends ViewPaneContainer {
// Register views in priority order: local, history, then alphabetically sorted others
const orderedProviders = [
...(localProvider ? [{ provider: localProvider, displayName: 'Local Chat Sessions', baseOrder: 0 }] : []),
...(localProvider ? [{ provider: localProvider, displayName: 'Local Chat Agent', baseOrder: 0 }] : []),
...(historyProvider ? [{ provider: historyProvider, displayName: 'History', baseOrder: 1, when: undefined }] : []),
...providersWithDisplayNames.map((item, index) => ({
...item,
@@ -286,7 +286,7 @@ class ChatSessionsViewPaneContainer extends ViewPaneContainer {
if (provider.chatSessionType === 'local') {
const viewsRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
this._register(viewsRegistry.registerViewWelcomeContent(viewDescriptor.id, {
content: nls.localize('chatSessions.noResults', "No local chat sessions\n[Start a Chat](command:{0})", ACTION_ID_OPEN_CHAT),
content: nls.localize('chatSessions.noResults', "No local chat agent sessions\n[Start an Agent Session](command:{0})", ACTION_ID_OPEN_CHAT),
}));
}
}
@@ -336,7 +336,7 @@ export class SessionsViewPane extends ViewPane {
if (elements.length === 1) {
return elements[0].label;
}
return nls.localize('chatSessions.dragLabel', "{0} chat sessions", elements.length);
return nls.localize('chatSessions.dragLabel', "{0} agent sessions", elements.length);
},
drop: () => { },
onDragOver: () => false,
@@ -215,9 +215,9 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
else if (chatSessionsInProgressCount > 0) {
text = '$(copilot-in-progress)';
if (chatSessionsInProgressCount > 1) {
ariaLabel = localize('chatSessionsInProgressStatus', "{0} chat sessions in progress", chatSessionsInProgressCount);
ariaLabel = localize('chatSessionsInProgressStatus', "{0} agent sessions in progress", chatSessionsInProgressCount);
} else {
ariaLabel = localize('chatSessionInProgressStatus', "1 chat session in progress");
ariaLabel = localize('chatSessionInProgressStatus', "1 agent session in progress");
}
}
@@ -66,6 +66,7 @@ import { IChatModeService } from '../common/chatModes.js';
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js';
import { ChatRequestParser } from '../common/chatRequestParser.js';
import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js';
import { IChatSessionsService } from '../common/chatSessionsService.js';
import { IChatSlashCommandService } from '../common/chatSlashCommands.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../common/chatVariableEntries.js';
import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js';
@@ -482,6 +483,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
@ICommandService private readonly commandService: ICommandService,
@IHoverService private readonly hoverService: IHoverService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
) {
super();
this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);
@@ -1356,15 +1358,22 @@ export class ChatWidget extends Disposable implements IChatWidget {
if (this.isLockedToCodingAgent) {
// TODO(jospicer): Let extensions contribute this welcome message/docs
const message = this._codingAgentPrefix === '@copilot '
? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._codingAgentPrefix, 'https://aka.ms/coding-agent-docs') + this.chatDisclaimer, { isTrusted: true })
: new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._codingAgentPrefix) + this.chatDisclaimer);
// Check for provider-specific customizations from chat sessions service
const providerIcon = this._lockedAgentId ? this.chatSessionsService.getIconForSessionType(this._lockedAgentId) : undefined;
const providerTitle = this._lockedAgentId ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgentId) : undefined;
const providerMessage = this._lockedAgentId ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgentId) : undefined;
// Fallback to default messages if provider doesn't specify
const message = providerMessage
? new MarkdownString(providerMessage)
: (this._codingAgentPrefix === '@copilot '
? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._codingAgentPrefix, 'https://aka.ms/coding-agent-docs') + this.chatDisclaimer, { isTrusted: true })
: new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._codingAgentPrefix) + this.chatDisclaimer));
return {
title: localize('codingAgentTitle', "Delegate to {0}", this._codingAgentPrefix),
title: providerTitle ?? localize('codingAgentTitle', "Delegate to {0}", this._codingAgentPrefix),
message,
icon: Codicon.sendToRemoteAgent,
icon: providerIcon ?? Codicon.sendToRemoteAgent,
additionalMessage,
};
}
@@ -1412,14 +1421,20 @@ export class ChatWidget extends Disposable implements IChatWidget {
additionalMessage = localize('expChatAdditionalMessage', "AI responses may be inaccurate.");
}
// Check for provider-specific customizations
const providerIcon = this._lockedAgentId ? this.chatSessionsService.getIconForSessionType(this._lockedAgentId) : undefined;
const providerTitle = this._lockedAgentId ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgentId) : undefined;
const providerMessage = this._lockedAgentId ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgentId) : undefined;
const suggestedPrompts = this._lockedAgentId ? undefined : this.getNewSuggestedPrompts();
const welcomeContent: IChatViewWelcomeContent = {
title: localize('expChatTitle', 'Build with agent mode'),
message: new MarkdownString(localize('expchatMessage', "Let's get started")),
icon: Codicon.chatSparkle,
title: providerTitle ?? localize('expChatTitle', 'Build with agent mode'),
message: providerMessage ? new MarkdownString(providerMessage) : new MarkdownString(localize('expchatMessage', "Let's get started")),
icon: providerIcon ?? Codicon.chatSparkle,
inputPart: this.inputPart.element,
additionalMessage,
isNew: true,
suggestedPrompts: this.getNewSuggestedPrompts(),
suggestedPrompts
};
return welcomeContent;
}
@@ -2268,8 +2283,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.container.setAttribute('data-session-id', model.sessionId);
this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection);
if (this._lockedToCodingAgent) {
const placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedToCodingAgent);
if (this._lockedAgentId) {
let placeholder = this._lockedAgentId ? this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgentId) : undefined;
if (!placeholder) {
placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgentId);
}
this.viewModel.setInputPlaceholder(placeholder);
this.inputEditor.updateOptions({ placeholder });
} else if (this.viewModel.inputPlaceholder) {
@@ -48,6 +48,10 @@ export interface IChatSessionsExtensionPoint {
readonly description: string;
readonly extensionDescription: IRelaxedExtensionDescription;
readonly when?: string;
readonly icon?: string;
readonly welcomeTitle?: string;
readonly welcomeMessage?: string;
readonly inputPlaceholder?: string;
readonly capabilities?: {
supportsFileAttachments?: boolean;
supportsToolAttachments?: boolean;
@@ -124,6 +128,10 @@ export interface IChatSessionsService {
getAllChatSessionContributions(): IChatSessionsExtensionPoint[];
canResolveItemProvider(chatSessionType: string): Promise<boolean>;
getAllChatSessionItemProviders(): IChatSessionItemProvider[];
getIconForSessionType(chatSessionType: string): ThemeIcon | undefined;
getWelcomeTitleForSessionType(chatSessionType: string): string | undefined;
getWelcomeMessageForSessionType(chatSessionType: string): string | undefined;
getInputPlaceholderForSessionType(chatSessionType: string): string | undefined;
provideNewChatSessionItem(chatSessionType: string, options: {
request: IChatAgentRequest;
metadata?: any;