Merge pull request #300587 from mjbvz/dev/mjbvz/ill-herring

Reduce `IChatSessionsService` interface size
This commit is contained in:
Matt Bierner
2026-03-10 16:59:38 -07:00
committed by GitHub
8 changed files with 98 additions and 143 deletions

View File

@@ -36,7 +36,7 @@ import { ChatModel } from '../../common/model/chatModel.js';
import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';
import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../attachments/chatVariables.js';
import { ChatSendResult, IChatService } from '../../common/chatService/chatService.js';
import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
import { ResolvedChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';
@@ -215,7 +215,7 @@ export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionV
};
}
private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction {
private static toAction(provider: AgentSessionProviders, contrib: ResolvedChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction {
return {
id: contrib.type,
enabled: true,
@@ -271,7 +271,7 @@ const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';
export class CreateRemoteAgentJobAction {
constructor() { }
private openUntitledEditor(commandService: ICommandService, continuationTarget: IChatSessionsExtensionPoint) {
private openUntitledEditor(commandService: ICommandService, continuationTarget: ResolvedChatSessionsExtensionPoint) {
commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`);
}
@@ -367,7 +367,7 @@ export class CreateRemoteAgentJobAction {
return undefined;
}
async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint, _widget?: IChatWidget) {
async run(accessor: ServicesAccessor, continuationTarget: ResolvedChatSessionsExtensionPoint, _widget?: IChatWidget) {
const contextKeyService = accessor.get(IContextKeyService);
const commandService = accessor.get(ICommandService);
const widgetService = accessor.get(IChatWidgetService);
@@ -512,7 +512,7 @@ export class CreateRemoteAgentJobAction {
class CreateRemoteAgentJobFromEditorAction {
constructor() { }
async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {
async run(accessor: ServicesAccessor, continuationTarget: ResolvedChatSessionsExtensionPoint) {
try {
const editorService = accessor.get(IEditorService);

View File

@@ -26,7 +26,7 @@ import { IWorkspaceTrustManagementService } from '../../../../../platform/worksp
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../../services/output/common/output.js';
import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionFileChange2, IChatSessionItem, IChatSessionsService, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js';
import { IChatWidgetService } from '../chat.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName, isBuiltInAgentSessionProvider } from './agentSessions.js';
@@ -486,7 +486,7 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
* Update the sessions by fetching from the service. This does not trigger an explicit refresh
*/
private async updateItems(providerFilter: readonly string[] | undefined, token: CancellationToken): Promise<void> {
const mapSessionContributionToType = new Map<string, IChatSessionsExtensionPoint>();
const mapSessionContributionToType = new Map<string, ResolvedChatSessionsExtensionPoint>();
for (const contribution of this.chatSessionsService.getAllChatSessionContributions()) {
mapSessionContributionToType.set(contribution.type, contribution);
}

View File

@@ -31,7 +31,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten
import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentRequest, IChatAgentService } from '../../common/participants/chatAgents.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js';
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
@@ -297,11 +297,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
private readonly inProgressMap: Map<string, number> = new Map();
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
private readonly _sessionTypeNewSessionOptions: Map<string, Record<string, string | IChatSessionProviderOptionItem>> = new Map();
private readonly _sessionTypeIcons: Map<string, ThemeIcon | { light: URI; dark: URI }> = new Map();
private readonly _sessionTypeWelcomeTitles: Map<string, string> = new Map();
private readonly _sessionTypeWelcomeMessages: Map<string, string> = new Map();
private readonly _sessionTypeWelcomeTips: Map<string, string> = new Map();
private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
private readonly _resourceAliases = new ResourceMap<URI>(); // real resource -> untitled resource
@@ -409,7 +404,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
this._logService.info(`[ChatSessionsService] registerContribution called for type='${contribution.type}', canDelegate=${contribution.canDelegate}, when='${contribution.when}', extension='${ext.identifier.value}'`);
if (this._contributions.has(contribution.type)) {
this._logService.info(`[ChatSessionsService] registerContribution: type='${contribution.type}' already registered, skipping`);
return { dispose: () => { } };
return Disposable.None;
}
// Track context keys from the when condition
@@ -434,41 +429,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
}
}
// Store icon mapping if provided
let icon: ThemeIcon | { dark: URI; light: URI } | undefined;
if (contribution.icon) {
// Parse icon string - support ThemeIcon format or file path from extension
if (typeof contribution.icon === 'string') {
icon = contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')
? ThemeIcon.fromString(contribution.icon)
: ThemeIcon.fromId(contribution.icon);
} else {
icon = {
dark: resources.joinPath(ext.extensionLocation, contribution.icon.dark),
light: resources.joinPath(ext.extensionLocation, contribution.icon.light)
};
}
}
if (icon) {
this._sessionTypeIcons.set(contribution.type, icon);
}
// Store welcome title, message, tips, 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.welcomeTips) {
this._sessionTypeWelcomeTips.set(contribution.type, contribution.welcomeTips);
}
if (contribution.inputPlaceholder) {
this._sessionTypeInputPlaceholders.set(contribution.type, contribution.inputPlaceholder);
}
this._evaluateAvailability();
return {
@@ -482,11 +442,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
}
}
}
this._sessionTypeIcons.delete(contribution.type);
this._sessionTypeWelcomeTitles.delete(contribution.type);
this._sessionTypeWelcomeMessages.delete(contribution.type);
this._sessionTypeWelcomeTips.delete(contribution.type);
this._sessionTypeInputPlaceholders.delete(contribution.type);
this._contributionDisposables.deleteAndDispose(contribution.type);
this._updateHasCanDelegateProvidersContextKey();
}
@@ -703,19 +658,19 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
}
private _registerAgent(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {
const { type: id, name, displayName, description } = contribution;
const storedIcon = this._sessionTypeIcons.get(id);
const storedIcon = this.getContributionIcon(ext, contribution);
const icons = ThemeIcon.isThemeIcon(storedIcon)
? { themeIcon: storedIcon, icon: undefined, iconDark: undefined }
: storedIcon
? { icon: storedIcon.light, iconDark: storedIcon.dark }
: { themeIcon: Codicon.sendToRemoteAgent };
const id = contribution.type;
const agentData: IChatAgentData = {
id,
name,
fullName: displayName,
description: description,
name: contribution.name,
fullName: contribution.displayName,
description: contribution.description,
isDefault: false,
isCore: false,
isDynamic: true,
@@ -737,9 +692,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
return this._chatAgentService.registerAgent(id, agentData);
}
getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {
return Array.from(this._contributions.values(), x => x.contribution)
.filter(contribution => this._isContributionAvailable(contribution));
getAllChatSessionContributions(): ResolvedChatSessionsExtensionPoint[] {
return Array.from(this._contributions.values())
.filter(entry => this._isContributionAvailable(entry.contribution))
.map(entry => this.resolveChatSessionContribution(entry.extension, entry.contribution));
}
private _updateHasCanDelegateProvidersContextKey(): void {
@@ -749,15 +705,56 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
this._hasCanDelegateProvidersKey.set(canDelegateEnabled);
}
getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
const contribution = this._contributions.get(chatSessionType)?.contribution;
if (!contribution) {
getChatSessionContribution(chatSessionType: string): ResolvedChatSessionsExtensionPoint | undefined {
const entry = this._contributions.get(chatSessionType);
if (!entry) {
return undefined;
}
return this._isContributionAvailable(contribution) ? contribution : undefined;
if (!this._isContributionAvailable(entry.contribution)) {
return undefined;
}
return this.resolveChatSessionContribution(entry.extension, entry.contribution);
}
private resolveChatSessionContribution(ext: IRelaxedExtensionDescription, contribution: IChatSessionsExtensionPoint) {
return {
...contribution,
icon: this.resolveIconForCurrentColorTheme(this.getContributionIcon(ext, contribution)),
};
}
private getContributionIcon(ext: IRelaxedExtensionDescription, contribution: IChatSessionsExtensionPoint): ThemeIcon | { light: URI; dark: URI } | undefined {
if (!contribution.icon) {
return undefined;
}
if (typeof contribution.icon === 'string') {
return contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')
? ThemeIcon.fromString(contribution.icon)
: ThemeIcon.fromId(contribution.icon);
}
return {
dark: resources.joinPath(ext.extensionLocation, contribution.icon.dark),
light: resources.joinPath(ext.extensionLocation, contribution.icon.light)
};
}
private resolveIconForCurrentColorTheme(rawIcon: ThemeIcon | { light: URI; dark: URI } | undefined) {
if (!rawIcon) {
return undefined;
}
if (ThemeIcon.isThemeIcon(rawIcon)) {
return rawIcon;
} else if (isDark(this._themeService.getColorTheme().type)) {
return rawIcon.dark;
} else {
return rawIcon.light;
}
}
async activateChatSessionItemProvider(chatViewType: string): Promise<void> {
await this.doActivateChatSessionItemController(chatViewType);
}
@@ -1119,44 +1116,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
this._logService.trace(`[ChatSessionsService] notifySessionOptionsChange: finished for ${sessionResource}`);
}
/**
* Get the icon for a specific session type
*/
public getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined {
const sessionTypeIcon = this._sessionTypeIcons.get(chatSessionType);
if (ThemeIcon.isThemeIcon(sessionTypeIcon)) {
return sessionTypeIcon;
}
if (isDark(this._themeService.getColorTheme().type)) {
return sessionTypeIcon?.dark;
} else {
return sessionTypeIcon?.light;
}
}
/**
* 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);
}
/**
* Get the capabilities for a specific session type
*/
@@ -1186,6 +1145,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
function registerNewSessionInPlaceAction(type: string, displayName: string): IDisposable {
return registerAction2(class NewChatSessionInPlaceAction extends Action2 {
constructor() {
@@ -1341,3 +1302,4 @@ export function getResourceForNewChatSession(options: NewChatSessionOpenOptions)
function isAgentSessionProviderType(type: string): boolean {
return Object.values(AgentSessionProviders).includes(type as AgentSessionProviders);
}

View File

@@ -1113,9 +1113,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent {
if (this.isLockedToCodingAgent) {
// Check for provider-specific customizations from chat sessions service
const providerIcon = this._lockedAgent ? this.chatSessionsService.getIconForSessionType(this._lockedAgent.id) : undefined;
const providerTitle = this._lockedAgent ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgent.id) : undefined;
const providerMessage = this._lockedAgent ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgent.id) : undefined;
const contribution = this._lockedAgent ? this.chatSessionsService.getChatSessionContribution(this._lockedAgent.id) : undefined;
const providerIcon = contribution?.icon;
const providerTitle = contribution?.welcomeTitle;
const providerMessage = contribution?.welcomeMessage;
// Fallback to default messages if provider doesn't specify
const message = providerMessage
@@ -1966,7 +1967,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.listWidget.setViewModel(this.viewModel);
if (this._lockedAgent) {
let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id);
let placeholder = this.chatSessionsService.getChatSessionContribution(this._lockedAgent.id)?.inputPlaceholder;
if (!placeholder) {
placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgent.id);
}

View File

@@ -194,10 +194,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler
// 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 !== localChatSessionType) {
const typeIcon = this.chatSessionsService.getIconForSessionType(sessionType);
if (typeIcon) {
return typeIcon;
}
return this.chatSessionsService.getChatSessionContribution(sessionType)?.icon;
}
return undefined;

View File

@@ -750,7 +750,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
const contribution = this.chatSessionsService.getChatSessionContribution(sessionType);
if (contribution) {
this._widget.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type);
this._widget.lockToCodingAgent(contribution.name, contribution.displayName, sessionType);
} else {
this._widget.unlockFromCodingAgent();
}

View File

@@ -213,6 +213,12 @@ export interface IChatSessionOptionsWillNotifyExtensionEvent extends IWaitUntil
readonly updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>;
}
export type ResolvedChatSessionsExtensionPoint = Omit<IChatSessionsExtensionPoint, 'icon'> & {
readonly icon: ThemeIcon | URI | undefined;
};
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');
export interface IChatSessionsService {
readonly _serviceBrand: undefined;
@@ -223,17 +229,12 @@ export interface IChatSessionsService {
readonly onDidChangeAvailability: Event<void>;
readonly onDidChangeInProgress: Event<void>;
getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined;
getChatSessionContribution(chatSessionType: string): ResolvedChatSessionsExtensionPoint | undefined;
getAllChatSessionContributions(): ResolvedChatSessionsExtensionPoint[];
registerChatSessionItemController(chatSessionType: string, controller: IChatSessionItemController): IDisposable;
activateChatSessionItemProvider(chatSessionType: string): Promise<void>;
getAllChatSessionContributions(): IChatSessionsExtensionPoint[];
getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined;
getWelcomeTitleForSessionType(chatSessionType: string): string | undefined;
getWelcomeMessageForSessionType(chatSessionType: string): string | undefined;
getInputPlaceholderForSessionType(chatSessionType: string): string | undefined;
/**
* Get the list of current chat session items grouped by session type.
* @param providerTypeFilter If specified, only returns items from the given providers. If undefined, returns items from all providers.
@@ -322,4 +323,3 @@ export function isIChatSessionFileChange2(obj: unknown): obj is IChatSessionFile
return candidate && candidate.uri instanceof URI && typeof candidate.insertions === 'number' && typeof candidate.deletions === 'number';
}
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');

View File

@@ -11,7 +11,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
import { URI } from '../../../../../base/common/uri.js';
import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/participants/chatAgents.js';
import { IChatModel } from '../../common/model/chatModel.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';
import { IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js';
import { Target } from '../../common/promptSyntax/promptTypes.js';
export class MockChatSessionsService implements IChatSessionsService {
@@ -73,12 +73,24 @@ export class MockChatSessionsService implements IChatSessionsService {
};
}
getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {
return this.contributions;
getAllChatSessionContributions(): ResolvedChatSessionsExtensionPoint[] {
return this.contributions.map(contribution => this.resolveContribution(contribution));
}
getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {
return this.contributions.find(contrib => contrib.type === chatSessionType);
getChatSessionContribution(chatSessionType: string): ResolvedChatSessionsExtensionPoint | undefined {
const contribution = this.contributions.find(c => c.type === chatSessionType);
if (!contribution) {
return undefined;
}
return this.resolveContribution(contribution);
}
private resolveContribution(contribution: IChatSessionsExtensionPoint): ResolvedChatSessionsExtensionPoint {
return {
...contribution,
icon: contribution.icon && typeof contribution.icon === 'string' ? ThemeIcon.fromId(contribution.icon) : undefined,
};
}
setContributions(contributions: IChatSessionsExtensionPoint[]): void {
@@ -89,23 +101,6 @@ export class MockChatSessionsService implements IChatSessionsService {
// Noop, nothing to activate
}
getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined {
const contribution = this.contributions.find(c => c.type === chatSessionType);
return contribution?.icon && typeof contribution.icon === 'string' ? ThemeIcon.fromId(contribution.icon) : undefined;
}
getWelcomeTitleForSessionType(chatSessionType: string): string | undefined {
return this.contributions.find(c => c.type === chatSessionType)?.welcomeTitle;
}
getWelcomeMessageForSessionType(chatSessionType: string): string | undefined {
return this.contributions.find(c => c.type === chatSessionType)?.welcomeMessage;
}
getInputPlaceholderForSessionType(chatSessionType: string): string | undefined {
return this.contributions.find(c => c.type === chatSessionType)?.inputPlaceholder;
}
getChatSessionItems(providerTypeFilter: readonly string[] | undefined, token: CancellationToken): Promise<Array<{ readonly chatSessionType: string; readonly items: readonly IChatSessionItem[] }>> {
return Promise.all(
Array.from(this.sessionItemControllers.entries())