mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-18 23:59:43 +01:00
1037 lines
48 KiB
TypeScript
1037 lines
48 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';
|
|
import { raceTimeout, timeout } from '../../../../../base/common/async.js';
|
|
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
|
import { Codicon } from '../../../../../base/common/codicons.js';
|
|
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
|
|
import { Emitter, Event } from '../../../../../base/common/event.js';
|
|
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
|
import { Lazy } from '../../../../../base/common/lazy.js';
|
|
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
|
|
import { URI } from '../../../../../base/common/uri.js';
|
|
import { localize, localize2 } from '../../../../../nls.js';
|
|
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
|
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
|
import { ILogService } from '../../../../../platform/log/common/log.js';
|
|
import product from '../../../../../platform/product/common/product.js';
|
|
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
|
import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js';
|
|
import { IWorkbenchEnvironmentService } from '../../../../services/environment/common/environmentService.js';
|
|
import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
|
|
import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js';
|
|
import { IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../../common/participants/chatAgents.js';
|
|
import { ChatEntitlement, ChatEntitlementContext, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
|
|
import { ChatModel, ChatRequestModel, IChatRequestModel, IChatRequestVariableData } from '../../common/model/chatModel.js';
|
|
import { ChatMode } from '../../common/chatModes.js';
|
|
import { ChatRequestAgentPart, ChatRequestToolPart } from '../../common/requestParser/chatParserTypes.js';
|
|
import { IChatProgress, IChatService } from '../../common/chatService/chatService.js';
|
|
import { IChatRequestToolEntry } from '../../common/attachments/chatVariableEntries.js';
|
|
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
|
|
import { ILanguageModelsService } from '../../common/languageModels.js';
|
|
import { CHAT_OPEN_ACTION_ID, CHAT_SETUP_ACTION_ID } from '../actions/chatActions.js';
|
|
import { ChatViewId, IChatWidgetService } from '../chat.js';
|
|
import { IViewsService } from '../../../../services/views/common/viewsService.js';
|
|
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
|
|
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
|
|
import { CodeAction, CodeActionList, Command, NewSymbolName, NewSymbolNameTriggerKind } from '../../../../../editor/common/languages.js';
|
|
import { ITextModel } from '../../../../../editor/common/model.js';
|
|
import { IRange, Range } from '../../../../../editor/common/core/range.js';
|
|
import { ISelection, Selection } from '../../../../../editor/common/core/selection.js';
|
|
import { ResourceMap } from '../../../../../base/common/map.js';
|
|
import { CodeActionKind } from '../../../../../editor/contrib/codeAction/common/types.js';
|
|
import { ACTION_START as INLINE_CHAT_START } from '../../../inlineChat/common/inlineChat.js';
|
|
import { IPosition } from '../../../../../editor/common/core/position.js';
|
|
import { IMarker, IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
|
|
import { ChatSetupController } from './chatSetupController.js';
|
|
import { ChatGlobalPerfMark, markChatGlobal } from '../../common/chatPerf.js';
|
|
import { ChatSetupAnonymous, ChatSetupStep, IChatSetupResult, maybeEnableAuthExtension, refreshTokens } from './chatSetup.js';
|
|
import { ChatSetup } from './chatSetupRunner.js';
|
|
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
|
|
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
|
|
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
|
|
import { IHostService } from '../../../../services/host/browser/host.js';
|
|
import { IOutputService } from '../../../../services/output/common/output.js';
|
|
import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
|
|
|
|
const defaultChat = {
|
|
extensionId: product.defaultChatAgent?.extensionId ?? '',
|
|
chatExtensionId: product.defaultChatAgent?.chatExtensionId ?? '',
|
|
provider: product.defaultChatAgent?.provider ?? { default: { id: '', name: '' }, enterprise: { id: '', name: '' }, apple: { id: '', name: '' }, google: { id: '', name: '' } },
|
|
outputChannelId: product.defaultChatAgent?.chatExtensionOutputId ?? '',
|
|
outputExtensionStateCommand: product.defaultChatAgent?.chatExtensionOutputExtensionStateCommand ?? '',
|
|
};
|
|
|
|
const ToolsAgentContextKey = ContextKeyExpr.and(
|
|
ContextKeyExpr.equals(`config.${ChatConfiguration.AgentEnabled}`, true),
|
|
ContextKeyExpr.not(`previewFeaturesDisabled`) // Set by extension
|
|
);
|
|
|
|
export class SetupAgent extends Disposable implements IChatAgentImplementation {
|
|
|
|
static registerDefaultAgents(instantiationService: IInstantiationService, location: ChatAgentLocation, mode: ChatModeKind, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
|
|
return instantiationService.invokeFunction(accessor => {
|
|
const chatAgentService = accessor.get(IChatAgentService);
|
|
|
|
let description;
|
|
if (mode === ChatModeKind.Ask) {
|
|
description = ChatMode.Ask.description.get();
|
|
} else if (mode === ChatModeKind.Edit) {
|
|
description = ChatMode.Edit.description.get();
|
|
} else {
|
|
description = ChatMode.Agent.description.get();
|
|
}
|
|
|
|
let id: string;
|
|
switch (location) {
|
|
case ChatAgentLocation.Chat:
|
|
if (mode === ChatModeKind.Ask) {
|
|
id = 'setup.chat';
|
|
} else if (mode === ChatModeKind.Edit) {
|
|
id = 'setup.edits';
|
|
} else {
|
|
id = 'setup.agent';
|
|
}
|
|
break;
|
|
case ChatAgentLocation.Terminal:
|
|
id = 'setup.terminal';
|
|
break;
|
|
case ChatAgentLocation.EditorInline:
|
|
id = 'setup.editor';
|
|
break;
|
|
case ChatAgentLocation.Notebook:
|
|
id = 'setup.notebook';
|
|
break;
|
|
}
|
|
|
|
return SetupAgent.doRegisterAgent(instantiationService, chatAgentService, id, `${defaultChat.provider.default.name} Copilot` /* Do NOT change, this hides the username altogether in Chat */, true, description, location, mode, context, controller);
|
|
});
|
|
}
|
|
|
|
static registerBuiltInAgents(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): IDisposable {
|
|
return instantiationService.invokeFunction(accessor => {
|
|
const chatAgentService = accessor.get(IChatAgentService);
|
|
|
|
const disposables = new DisposableStore();
|
|
|
|
// Register VSCode agent
|
|
const { disposable: vscodeDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.vscode', 'vscode', false, localize2('vscodeAgentDescription', "Ask questions about VS Code").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
|
|
disposables.add(vscodeDisposable);
|
|
|
|
// Register workspace agent
|
|
const { disposable: workspaceDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.workspace', 'workspace', false, localize2('workspaceAgentDescription', "Ask about your workspace").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
|
|
disposables.add(workspaceDisposable);
|
|
|
|
// Register terminal agent
|
|
const { disposable: terminalDisposable } = SetupAgent.doRegisterAgent(instantiationService, chatAgentService, 'setup.terminal.agent', 'terminal', false, localize2('terminalAgentDescription', "Ask how to do something in the terminal").value, ChatAgentLocation.Chat, ChatModeKind.Agent, context, controller);
|
|
disposables.add(terminalDisposable);
|
|
|
|
// Register tools
|
|
disposables.add(SetupTool.registerTool(instantiationService, {
|
|
id: 'setup_tools_createNewWorkspace',
|
|
source: ToolDataSource.Internal,
|
|
icon: Codicon.newFolder,
|
|
displayName: localize('setupToolDisplayName', "New Workspace"),
|
|
modelDescription: 'Scaffold a new workspace in VS Code',
|
|
userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
|
|
canBeReferencedInPrompt: true,
|
|
toolReferenceName: 'new',
|
|
when: ContextKeyExpr.true(),
|
|
}));
|
|
|
|
return disposables;
|
|
});
|
|
}
|
|
|
|
private static doRegisterAgent(instantiationService: IInstantiationService, chatAgentService: IChatAgentService, id: string, name: string, isDefault: boolean, description: string, location: ChatAgentLocation, mode: ChatModeKind, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): { agent: SetupAgent; disposable: IDisposable } {
|
|
const disposables = new DisposableStore();
|
|
disposables.add(chatAgentService.registerAgent(id, {
|
|
id,
|
|
name,
|
|
isDefault,
|
|
isCore: true,
|
|
modes: [mode],
|
|
when: mode === ChatModeKind.Agent ? ToolsAgentContextKey?.serialize() : undefined,
|
|
slashCommands: [],
|
|
disambiguation: [],
|
|
locations: [location],
|
|
metadata: { helpTextPrefix: SetupAgent.SETUP_NEEDED_MESSAGE },
|
|
description,
|
|
extensionId: nullExtensionDescription.identifier,
|
|
extensionVersion: undefined,
|
|
extensionDisplayName: nullExtensionDescription.name,
|
|
extensionPublisherId: nullExtensionDescription.publisher
|
|
}));
|
|
|
|
const agent = disposables.add(instantiationService.createInstance(SetupAgent, context, controller, location));
|
|
disposables.add(chatAgentService.registerAgentImplementation(id, agent));
|
|
if (mode === ChatModeKind.Agent) {
|
|
chatAgentService.updateAgent(id, { themeIcon: Codicon.tools });
|
|
}
|
|
|
|
return { agent, disposable: disposables };
|
|
}
|
|
|
|
private static readonly SETUP_NEEDED_MESSAGE = new MarkdownString(localize('settingUpCopilotNeeded', "You need to set up GitHub Copilot and be signed in to use Chat."));
|
|
private static readonly TRUST_NEEDED_MESSAGE = new MarkdownString(localize('trustNeeded', "You need to trust this workspace to use Chat."));
|
|
|
|
private static readonly CHAT_RETRY_COMMAND_ID = 'workbench.action.chat.retrySetup';
|
|
private static readonly CHAT_SHOW_OUTPUT_COMMAND_ID = 'workbench.action.chat.showOutput';
|
|
|
|
private readonly _onUnresolvableError = this._register(new Emitter<void>());
|
|
readonly onUnresolvableError = this._onUnresolvableError.event;
|
|
|
|
private readonly pendingForwardedRequests = new ResourceMap<Promise<void>>();
|
|
|
|
constructor(
|
|
private readonly context: ChatEntitlementContext,
|
|
private readonly controller: Lazy<ChatSetupController>,
|
|
private readonly location: ChatAgentLocation,
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
|
@ILogService private readonly logService: ILogService,
|
|
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
|
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
|
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
|
|
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
|
|
@IViewsService private readonly viewsService: IViewsService,
|
|
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
|
@IOutputService private readonly outputService: IOutputService,
|
|
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
|
|
@ICommandService private readonly commandService: ICommandService,
|
|
) {
|
|
super();
|
|
|
|
this.registerCommands();
|
|
}
|
|
|
|
private registerCommands(): void {
|
|
|
|
// Retry chat command
|
|
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_RETRY_COMMAND_ID, async (accessor, sessionResource: URI) => {
|
|
const hostService = accessor.get(IHostService);
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
|
|
const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);
|
|
await widget?.clear();
|
|
|
|
hostService.reload();
|
|
}));
|
|
|
|
// Show output command: execute extension state command if available, then show output channel
|
|
this._register(CommandsRegistry.registerCommand(SetupAgent.CHAT_SHOW_OUTPUT_COMMAND_ID, async (accessor) => {
|
|
const commandService = accessor.get(ICommandService);
|
|
|
|
if (defaultChat.outputExtensionStateCommand) {
|
|
// Command invocation may fail or is blocked by the extension activating
|
|
// so we just don't wait and timeout after a certain time, logging the error if it fails or times out.
|
|
raceTimeout(
|
|
commandService.executeCommand(defaultChat.outputExtensionStateCommand),
|
|
5000,
|
|
() => this.logService.info('[chat setup] Timed out executing extension state command')
|
|
).then(undefined, error => {
|
|
this.logService.info('[chat setup] Failed to execute extension state command', error);
|
|
});
|
|
}
|
|
|
|
if (defaultChat.outputChannelId) {
|
|
await commandService.executeCommand(`workbench.action.output.show.${defaultChat.outputChannelId}`);
|
|
}
|
|
}));
|
|
}
|
|
|
|
async invoke(request: IChatAgentRequest, progress: (parts: IChatProgress[]) => void): Promise<IChatAgentResult> {
|
|
return this.instantiationService.invokeFunction(async accessor /* using accessor for lazy loading */ => {
|
|
const chatService = accessor.get(IChatService);
|
|
const languageModelsService = accessor.get(ILanguageModelsService);
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const chatAgentService = accessor.get(IChatAgentService);
|
|
const languageModelToolsService = accessor.get(ILanguageModelToolsService);
|
|
const defaultAccountService = accessor.get(IDefaultAccountService);
|
|
|
|
return this.doInvoke(request, part => progress([part]), chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService);
|
|
});
|
|
}
|
|
|
|
private async doInvoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise<IChatAgentResult> {
|
|
if (
|
|
!this.context.state.installed || // Extension not installed: run setup to install
|
|
this.context.state.disabled || // Extension disabled: run setup to enable
|
|
this.context.state.untrusted || // Workspace untrusted: run setup to ask for trust
|
|
this.context.state.entitlement === ChatEntitlement.Available || // Entitlement available: run setup to sign up
|
|
(
|
|
this.context.state.entitlement === ChatEntitlement.Unknown && // Entitlement unknown: run setup to sign in / sign up
|
|
!this.chatEntitlementService.anonymous // unless anonymous access is enabled
|
|
)
|
|
) {
|
|
return this.doInvokeWithSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService, defaultAccountService);
|
|
}
|
|
|
|
return this.doInvokeWithoutSetup(request, progress, chatService, languageModelsService, chatWidgetService, chatAgentService, languageModelToolsService);
|
|
}
|
|
|
|
private async doInvokeWithoutSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService): Promise<IChatAgentResult> {
|
|
const requestModel = chatWidgetService.getWidgetBySessionResource(request.sessionResource)?.viewModel?.model.getRequests().at(-1);
|
|
if (!requestModel) {
|
|
this.logService.error('[chat setup] Request model not found, cannot redispatch request.');
|
|
return {}; // this should not happen
|
|
}
|
|
|
|
progress({
|
|
kind: 'progressMessage',
|
|
content: new MarkdownString(localize('waitingChat', "Getting chat ready")),
|
|
shimmer: true,
|
|
});
|
|
|
|
await this.forwardRequestToChat(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
|
|
|
|
return {};
|
|
}
|
|
|
|
private async forwardRequestToChat(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
|
|
try {
|
|
await this.doForwardRequestToChat(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
|
|
} catch (error) {
|
|
this.logService.error('[chat setup] Failed to forward request to chat', error);
|
|
|
|
progress({
|
|
kind: 'warning',
|
|
content: new MarkdownString(localize('copilotUnavailableWarning', "Failed to get a response. Please try again."))
|
|
});
|
|
}
|
|
}
|
|
|
|
private async doForwardRequestToChat(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
|
|
if (this.pendingForwardedRequests.has(requestModel.session.sessionResource)) {
|
|
throw new Error('Request already in progress');
|
|
}
|
|
|
|
const forwardRequest = this.doForwardRequestToChatWhenReady(requestModel, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
|
|
this.pendingForwardedRequests.set(requestModel.session.sessionResource, forwardRequest);
|
|
|
|
try {
|
|
await forwardRequest;
|
|
} finally {
|
|
this.pendingForwardedRequests.delete(requestModel.session.sessionResource);
|
|
}
|
|
}
|
|
|
|
private async doForwardRequestToChatWhenReady(requestModel: IChatRequestModel, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, chatWidgetService: IChatWidgetService, languageModelToolsService: ILanguageModelToolsService): Promise<void> {
|
|
|
|
// Ensure auth extension is enabled before waiting for chat readiness.
|
|
// This must run before the readiness event listeners are set up because
|
|
// updateRunningExtensions restarts all extension hosts.
|
|
const authExtensionReEnabled = await maybeEnableAuthExtension(this.extensionsWorkbenchService, this.logService);
|
|
if (authExtensionReEnabled) {
|
|
refreshTokens(this.commandService);
|
|
}
|
|
|
|
const widget = chatWidgetService.getWidgetBySessionResource(requestModel.session.sessionResource);
|
|
const modeInfo = widget?.input.currentModeInfo;
|
|
|
|
// We need a signal to know when we can resend the request to
|
|
// Chat. Waiting for the registration of the agent is not
|
|
// enough, we also need a language/tools model to be available.
|
|
|
|
let agentActivated = false;
|
|
let agentReady = false;
|
|
let languageModelReady = false;
|
|
let toolsModelReady = false;
|
|
|
|
markChatGlobal(ChatGlobalPerfMark.WillWaitForActivation);
|
|
|
|
const whenAgentActivated = this.whenAgentActivated(chatService).then(() => agentActivated = true);
|
|
const whenAgentReady = this.whenAgentReady(chatAgentService, modeInfo?.kind)?.then(() => agentReady = true);
|
|
if (!whenAgentReady) {
|
|
agentReady = true;
|
|
}
|
|
const whenLanguageModelReady = this.whenLanguageModelReady(languageModelsService, requestModel.modelId)?.then(() => languageModelReady = true);
|
|
if (!whenLanguageModelReady) {
|
|
languageModelReady = true;
|
|
}
|
|
const whenToolsModelReady = this.whenToolsModelReady(languageModelToolsService, requestModel)?.then(() => toolsModelReady = true);
|
|
if (!whenToolsModelReady) {
|
|
toolsModelReady = true;
|
|
}
|
|
|
|
if (whenLanguageModelReady instanceof Promise || whenAgentReady instanceof Promise || whenToolsModelReady instanceof Promise) {
|
|
const timeoutHandle = setTimeout(() => {
|
|
progress({
|
|
kind: 'progressMessage',
|
|
content: new MarkdownString(localize('waitingChat2', "Chat is almost ready")),
|
|
shimmer: true,
|
|
});
|
|
}, 10000);
|
|
|
|
const disposables = new DisposableStore();
|
|
disposables.add(toDisposable(() => clearTimeout(timeoutHandle)));
|
|
try {
|
|
const allReady = Promise.allSettled([
|
|
whenAgentActivated,
|
|
whenAgentReady,
|
|
whenLanguageModelReady,
|
|
whenToolsModelReady
|
|
]);
|
|
const ready = await Promise.race([
|
|
timeout(this.environmentService.remoteAuthority ? 60000 /* increase for remote scenarios */ : 20000).then(() => 'timedout'),
|
|
this.whenPanelAgentHasGuidance(disposables).then(() => 'panelGuidance'),
|
|
allReady
|
|
]);
|
|
|
|
if (ready === 'panelGuidance') {
|
|
const warningMessage = localize('chatTookLongWarningExtension', "Please try again.");
|
|
|
|
progress({
|
|
kind: 'markdownContent',
|
|
content: new MarkdownString(warningMessage)
|
|
});
|
|
|
|
// This means Chat is unhealthy and we cannot retry the
|
|
// request. Signal this to the outside via an event.
|
|
this._onUnresolvableError.fire();
|
|
return;
|
|
}
|
|
|
|
if (ready === 'timedout') {
|
|
let warningMessage: string;
|
|
if (this.chatEntitlementService.anonymous) {
|
|
warningMessage = localize('chatTookLongWarningAnonymous', "Chat took too long to get ready. Please ensure that the extension `{0}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.chatExtensionId);
|
|
} else {
|
|
warningMessage = localize('chatTookLongWarning', "Chat took too long to get ready. Please ensure you are signed in to {0} and that the extension `{1}` is installed and enabled. Click restart to try again if this issue persists.", defaultChat.provider.default.name, defaultChat.chatExtensionId);
|
|
}
|
|
|
|
const diagnosticInfo = this.computeDiagnosticInfo(agentActivated, agentReady, languageModelReady, toolsModelReady, requestModel, languageModelsService, chatAgentService, modeInfo);
|
|
|
|
this.logService.warn(`[chat setup] ${warningMessage}`, diagnosticInfo);
|
|
|
|
type ChatSetupTimeoutClassification = {
|
|
owner: 'chrmarti';
|
|
comment: 'Provides insight into chat setup timeouts.';
|
|
agentActivated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was activated.' };
|
|
agentReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was ready.' };
|
|
agentHasDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a default agent exists for the location and mode.' };
|
|
agentDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the default agent is a core agent.' };
|
|
agentHasContributedDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a contributed default agent exists for the location.' };
|
|
agentContributedDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the contributed default agent is a core agent.' };
|
|
agentActivatedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of activated agents at timeout.' };
|
|
agentLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat agent location.' };
|
|
agentModeKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat mode kind.' };
|
|
languageModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the language model was ready.' };
|
|
languageModelCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of registered language models at timeout.' };
|
|
languageModelDefaultCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of language models with isDefaultForLocation[Chat] set.' };
|
|
languageModelHasRequestedModel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a specific model ID was requested.' };
|
|
toolsModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the tools model was ready.' };
|
|
isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether this is a remote scenario.' };
|
|
isAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether anonymous access is enabled.' };
|
|
matchingWelcomeViewWhen: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The when clause of the matching extension welcome view, if any.' };
|
|
};
|
|
type ChatSetupTimeoutEvent = {
|
|
agentActivated: boolean;
|
|
agentReady: boolean;
|
|
agentHasDefault: boolean;
|
|
agentDefaultIsCore: boolean;
|
|
agentHasContributedDefault: boolean;
|
|
agentContributedDefaultIsCore: boolean;
|
|
agentActivatedCount: number;
|
|
agentLocation: string;
|
|
agentModeKind: string;
|
|
languageModelReady: boolean;
|
|
languageModelCount: number;
|
|
languageModelDefaultCount: number;
|
|
languageModelHasRequestedModel: boolean;
|
|
toolsModelReady: boolean;
|
|
isRemote: boolean;
|
|
isAnonymous: boolean;
|
|
matchingWelcomeViewWhen: string;
|
|
};
|
|
|
|
this.telemetryService.publicLog2<ChatSetupTimeoutEvent, ChatSetupTimeoutClassification>('chatSetup.timeout', diagnosticInfo);
|
|
|
|
progress({
|
|
kind: 'warning',
|
|
content: new MarkdownString(warningMessage)
|
|
});
|
|
|
|
if (defaultChat.outputChannelId && this.outputService.getChannelDescriptor(defaultChat.outputChannelId)) {
|
|
progress({
|
|
kind: 'command',
|
|
command: {
|
|
id: SetupAgent.CHAT_SHOW_OUTPUT_COMMAND_ID,
|
|
title: localize('showCopilotChatDetails', "Show Details")
|
|
}
|
|
});
|
|
} else {
|
|
this.logService.warn(defaultChat.outputChannelId
|
|
? `[chat setup] No output channel found for id '${defaultChat.outputChannelId}' to show details about chat setup timeout. Please ensure the ${defaultChat.chatExtensionId} extension is activated.`
|
|
: '[chat setup] No output channel provided via product.json to show details about chat setup timeout.');
|
|
progress({
|
|
kind: 'command',
|
|
command: {
|
|
id: SetupAgent.CHAT_RETRY_COMMAND_ID,
|
|
title: localize('retryChat', "Restart"),
|
|
arguments: [requestModel.session.sessionResource]
|
|
}
|
|
});
|
|
}
|
|
|
|
// Wait for all readiness signals and log/send
|
|
// telemetry about recovery after the timeout.
|
|
await allReady;
|
|
|
|
const recoveryDiagnosticInfo = this.computeDiagnosticInfo(agentActivated, agentReady, languageModelReady, toolsModelReady, requestModel, languageModelsService, chatAgentService, modeInfo);
|
|
|
|
this.logService.info('[chat setup] Chat setup timeout recovered', recoveryDiagnosticInfo);
|
|
|
|
type ChatSetupTimeoutRecoveryClassification = {
|
|
owner: 'chrmarti';
|
|
comment: 'Provides insight into chat setup timeout recovery.';
|
|
agentActivated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was activated.' };
|
|
agentReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the agent was ready.' };
|
|
agentHasDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a default agent exists for the location and mode.' };
|
|
agentDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the default agent is a core agent.' };
|
|
agentHasContributedDefault: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a contributed default agent exists for the location.' };
|
|
agentContributedDefaultIsCore: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the contributed default agent is a core agent.' };
|
|
agentActivatedCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of activated agents at recovery time.' };
|
|
agentLocation: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat agent location.' };
|
|
agentModeKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The chat mode kind.' };
|
|
languageModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the language model was ready.' };
|
|
languageModelCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of registered language models at recovery time.' };
|
|
languageModelDefaultCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of language models with isDefaultForLocation[Chat] set at recovery time.' };
|
|
languageModelHasRequestedModel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a specific model ID was requested.' };
|
|
toolsModelReady: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the tools model was ready.' };
|
|
isRemote: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether this is a remote scenario.' };
|
|
isAnonymous: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether anonymous access is enabled.' };
|
|
matchingWelcomeViewWhen: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The when clause of the matching extension welcome view, if any.' };
|
|
};
|
|
type ChatSetupTimeoutRecoveryEvent = {
|
|
agentActivated: boolean;
|
|
agentReady: boolean;
|
|
agentHasDefault: boolean;
|
|
agentDefaultIsCore: boolean;
|
|
agentHasContributedDefault: boolean;
|
|
agentContributedDefaultIsCore: boolean;
|
|
agentActivatedCount: number;
|
|
agentLocation: string;
|
|
agentModeKind: string;
|
|
languageModelReady: boolean;
|
|
languageModelCount: number;
|
|
languageModelDefaultCount: number;
|
|
languageModelHasRequestedModel: boolean;
|
|
toolsModelReady: boolean;
|
|
isRemote: boolean;
|
|
isAnonymous: boolean;
|
|
matchingWelcomeViewWhen: string;
|
|
};
|
|
|
|
this.telemetryService.publicLog2<ChatSetupTimeoutRecoveryEvent, ChatSetupTimeoutRecoveryClassification>('chatSetup.timeoutRecovery', recoveryDiagnosticInfo);
|
|
}
|
|
} finally {
|
|
disposables.dispose();
|
|
}
|
|
}
|
|
|
|
markChatGlobal(ChatGlobalPerfMark.DidWaitForActivation);
|
|
await chatService.resendRequest(requestModel, {
|
|
...widget?.getModeRequestOptions(),
|
|
modeInfo,
|
|
userSelectedModelId: widget?.input.currentLanguageModel
|
|
});
|
|
}
|
|
|
|
private async whenPanelAgentHasGuidance(disposables: DisposableStore): Promise<void> {
|
|
const panelAgentHasGuidance = () => chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));
|
|
|
|
if (panelAgentHasGuidance()) {
|
|
return;
|
|
}
|
|
|
|
return new Promise<void>(resolve => {
|
|
let descriptorKeys: Set<string> = new Set();
|
|
const updateDescriptorKeys = () => {
|
|
const descriptors = chatViewsWelcomeRegistry.get();
|
|
descriptorKeys = new Set(descriptors.flatMap(d => d.when.keys()));
|
|
};
|
|
updateDescriptorKeys();
|
|
|
|
const onDidChangeRegistry = Event.map(chatViewsWelcomeRegistry.onDidChange, () => 'registry' as const);
|
|
const onDidChangeRelevantContext = Event.map(
|
|
Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(descriptorKeys)),
|
|
() => 'context' as const
|
|
);
|
|
|
|
disposables.add(Event.any(
|
|
onDidChangeRegistry,
|
|
onDidChangeRelevantContext
|
|
)(source => {
|
|
if (source === 'registry') {
|
|
updateDescriptorKeys();
|
|
}
|
|
if (panelAgentHasGuidance()) {
|
|
resolve();
|
|
}
|
|
}));
|
|
});
|
|
}
|
|
|
|
private whenLanguageModelReady(languageModelsService: ILanguageModelsService, modelId: string | undefined): Promise<unknown> | void {
|
|
const hasModelForRequest = () => {
|
|
if (modelId) {
|
|
return !!languageModelsService.lookupLanguageModel(modelId);
|
|
}
|
|
|
|
for (const id of languageModelsService.getLanguageModelIds()) {
|
|
const model = languageModelsService.lookupLanguageModel(id);
|
|
if (model?.isDefaultForLocation[ChatAgentLocation.Chat]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
if (hasModelForRequest()) {
|
|
return;
|
|
}
|
|
|
|
return Event.toPromise(Event.filter(languageModelsService.onDidChangeLanguageModels, () => hasModelForRequest()));
|
|
}
|
|
|
|
private whenToolsModelReady(languageModelToolsService: ILanguageModelToolsService, requestModel: IChatRequestModel): Promise<unknown> | void {
|
|
const needsToolsModel = requestModel.message.parts.some(part => part instanceof ChatRequestToolPart);
|
|
if (!needsToolsModel) {
|
|
return; // No tools in this request, no need to check
|
|
}
|
|
|
|
// check that tools other than setup. and internal tools are registered.
|
|
for (const tool of languageModelToolsService.getAllToolsIncludingDisabled()) {
|
|
if (tool.id.startsWith('copilot_')) {
|
|
return; // we have tools!
|
|
}
|
|
}
|
|
|
|
return Event.toPromise(Event.filter(languageModelToolsService.onDidChangeTools, () => {
|
|
for (const tool of languageModelToolsService.getAllToolsIncludingDisabled()) {
|
|
if (tool.id.startsWith('copilot_')) {
|
|
return true; // we have tools!
|
|
}
|
|
}
|
|
|
|
return false; // no external tools found
|
|
}));
|
|
}
|
|
|
|
private whenAgentReady(chatAgentService: IChatAgentService, mode: ChatModeKind | undefined): Promise<unknown> | void {
|
|
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
|
|
if (defaultAgent && !defaultAgent.isCore) {
|
|
return; // we have a default agent from an extension!
|
|
}
|
|
|
|
return Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {
|
|
const defaultAgent = chatAgentService.getDefaultAgent(this.location, mode);
|
|
return Boolean(defaultAgent && !defaultAgent.isCore);
|
|
}));
|
|
}
|
|
|
|
private async whenAgentActivated(chatService: IChatService): Promise<void> {
|
|
try {
|
|
await chatService.activateDefaultAgent(this.location);
|
|
} catch (error) {
|
|
this.logService.error(error);
|
|
}
|
|
}
|
|
|
|
private computeDiagnosticInfo(agentActivated: boolean, agentReady: boolean, languageModelReady: boolean, toolsModelReady: boolean, requestModel: IChatRequestModel, languageModelsService: ILanguageModelsService, chatAgentService: IChatAgentService, modeInfo: { kind?: ChatModeKind } | undefined) {
|
|
const languageModelIds = languageModelsService.getLanguageModelIds();
|
|
let languageModelDefaultCount = 0;
|
|
for (const id of languageModelIds) {
|
|
const model = languageModelsService.lookupLanguageModel(id);
|
|
if (model?.isDefaultForLocation[ChatAgentLocation.Chat]) {
|
|
languageModelDefaultCount++;
|
|
}
|
|
}
|
|
|
|
const defaultAgent = chatAgentService.getDefaultAgent(this.location, modeInfo?.kind);
|
|
const contributedDefaultAgent = chatAgentService.getContributedDefaultAgent(this.location);
|
|
const chatViewPane = this.viewsService.getActiveViewWithId(ChatViewId) as ChatViewPane | undefined;
|
|
const matchingWelcomeView = chatViewPane?.getMatchingWelcomeView();
|
|
|
|
return {
|
|
agentActivated,
|
|
agentReady,
|
|
agentHasDefault: !!defaultAgent,
|
|
agentDefaultIsCore: defaultAgent?.isCore ?? false,
|
|
agentHasContributedDefault: !!contributedDefaultAgent,
|
|
agentContributedDefaultIsCore: contributedDefaultAgent?.isCore ?? false,
|
|
agentActivatedCount: chatAgentService.getActivatedAgents().length,
|
|
agentLocation: this.location,
|
|
agentModeKind: modeInfo?.kind ?? '',
|
|
languageModelReady,
|
|
languageModelCount: languageModelIds.length,
|
|
languageModelDefaultCount,
|
|
languageModelHasRequestedModel: !!requestModel.modelId,
|
|
toolsModelReady,
|
|
isRemote: !!this.environmentService.remoteAuthority,
|
|
isAnonymous: this.chatEntitlementService.anonymous,
|
|
matchingWelcomeViewWhen: matchingWelcomeView?.when.serialize() ?? (chatViewPane ? 'noWelcomeView' : 'noChatViewPane'),
|
|
};
|
|
}
|
|
|
|
private async doInvokeWithSetup(request: IChatAgentRequest, progress: (part: IChatProgress) => void, chatService: IChatService, languageModelsService: ILanguageModelsService, chatWidgetService: IChatWidgetService, chatAgentService: IChatAgentService, languageModelToolsService: ILanguageModelToolsService, defaultAccountService: IDefaultAccountService): Promise<IChatAgentResult> {
|
|
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'chat' });
|
|
|
|
const widget = chatWidgetService.getWidgetBySessionResource(request.sessionResource);
|
|
const requestModel = widget?.viewModel?.model.getRequests().at(-1);
|
|
|
|
const setupListener = Event.runAndSubscribe(this.controller.value.onDidChange, (() => {
|
|
switch (this.controller.value.step) {
|
|
case ChatSetupStep.SigningIn:
|
|
progress({
|
|
kind: 'progressMessage',
|
|
content: new MarkdownString(localize('setupChatSignIn2', "Signing in to {0}", defaultAccountService.getDefaultAccountAuthenticationProvider().name)),
|
|
shimmer: true,
|
|
});
|
|
break;
|
|
case ChatSetupStep.Installing:
|
|
progress({
|
|
kind: 'progressMessage',
|
|
content: new MarkdownString(localize('installingChat', "Getting chat ready")),
|
|
shimmer: true,
|
|
});
|
|
break;
|
|
}
|
|
}));
|
|
|
|
let result: IChatSetupResult | undefined = undefined;
|
|
try {
|
|
result = await ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({
|
|
disableChatViewReveal: true, // we are already in a chat context
|
|
forceAnonymous: this.chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithoutDialog : undefined // only enable anonymous selectively
|
|
});
|
|
} catch (error) {
|
|
this.logService.error(`[chat setup] Error during setup: ${toErrorMessage(error)}`);
|
|
} finally {
|
|
setupListener.dispose();
|
|
}
|
|
|
|
// User has agreed to run the setup
|
|
if (typeof result?.success === 'boolean') {
|
|
if (result.success) {
|
|
if (result.dialogSkipped) {
|
|
await widget?.clear(); // make room for the Chat welcome experience
|
|
} else if (requestModel) {
|
|
let newRequest = this.replaceAgentInRequestModel(requestModel, chatAgentService); // Replace agent part with the actual Chat agent...
|
|
newRequest = this.replaceToolInRequestModel(newRequest); // ...then replace any tool parts with the actual Chat tools
|
|
|
|
await this.forwardRequestToChat(newRequest, progress, chatService, languageModelsService, chatAgentService, chatWidgetService, languageModelToolsService);
|
|
}
|
|
} else {
|
|
progress({
|
|
kind: 'warning',
|
|
content: new MarkdownString(localize('chatSetupError', "Chat setup failed."))
|
|
});
|
|
}
|
|
}
|
|
|
|
// User has cancelled the setup
|
|
else {
|
|
progress({
|
|
kind: 'markdownContent',
|
|
content: this.workspaceTrustManagementService.isWorkspaceTrusted() ? SetupAgent.SETUP_NEEDED_MESSAGE : SetupAgent.TRUST_NEEDED_MESSAGE
|
|
});
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
private replaceAgentInRequestModel(requestModel: IChatRequestModel, chatAgentService: IChatAgentService): IChatRequestModel {
|
|
const agentPart = requestModel.message.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);
|
|
if (!agentPart) {
|
|
return requestModel;
|
|
}
|
|
|
|
const agentId = agentPart.agent.id.replace(/setup\./, `${defaultChat.extensionId}.`.toLowerCase());
|
|
const githubAgent = chatAgentService.getAgent(agentId);
|
|
if (!githubAgent) {
|
|
return requestModel;
|
|
}
|
|
|
|
const newAgentPart = new ChatRequestAgentPart(agentPart.range, agentPart.editorRange, githubAgent);
|
|
|
|
return new ChatRequestModel({
|
|
session: requestModel.session as ChatModel,
|
|
message: {
|
|
parts: requestModel.message.parts.map(part => {
|
|
if (part instanceof ChatRequestAgentPart) {
|
|
return newAgentPart;
|
|
}
|
|
return part;
|
|
}),
|
|
text: requestModel.message.text
|
|
},
|
|
variableData: requestModel.variableData,
|
|
timestamp: Date.now(),
|
|
attempt: requestModel.attempt,
|
|
modeInfo: requestModel.modeInfo,
|
|
confirmation: requestModel.confirmation,
|
|
locationData: requestModel.locationData,
|
|
attachedContext: requestModel.attachedContext,
|
|
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
|
|
});
|
|
}
|
|
|
|
private replaceToolInRequestModel(requestModel: IChatRequestModel): IChatRequestModel {
|
|
const toolPart = requestModel.message.parts.find((r): r is ChatRequestToolPart => r instanceof ChatRequestToolPart);
|
|
if (!toolPart) {
|
|
return requestModel;
|
|
}
|
|
|
|
const toolId = toolPart.toolId.replace(/setup.tools\./, `copilot_`.toLowerCase());
|
|
const newToolPart = new ChatRequestToolPart(
|
|
toolPart.range,
|
|
toolPart.editorRange,
|
|
toolPart.toolName,
|
|
toolId,
|
|
toolPart.displayName,
|
|
toolPart.icon
|
|
);
|
|
|
|
const chatRequestToolEntry: IChatRequestToolEntry = {
|
|
id: toolId,
|
|
name: 'new',
|
|
range: toolPart.range,
|
|
kind: 'tool',
|
|
value: undefined
|
|
};
|
|
|
|
const variableData: IChatRequestVariableData = {
|
|
variables: [chatRequestToolEntry]
|
|
};
|
|
|
|
return new ChatRequestModel({
|
|
session: requestModel.session as ChatModel,
|
|
message: {
|
|
parts: requestModel.message.parts.map(part => {
|
|
if (part instanceof ChatRequestToolPart) {
|
|
return newToolPart;
|
|
}
|
|
return part;
|
|
}),
|
|
text: requestModel.message.text
|
|
},
|
|
variableData: variableData,
|
|
timestamp: Date.now(),
|
|
attempt: requestModel.attempt,
|
|
modeInfo: requestModel.modeInfo,
|
|
confirmation: requestModel.confirmation,
|
|
locationData: requestModel.locationData,
|
|
attachedContext: [chatRequestToolEntry],
|
|
isCompleteAddedRequest: requestModel.isCompleteAddedRequest,
|
|
});
|
|
}
|
|
}
|
|
|
|
export class SetupTool implements IToolImpl {
|
|
|
|
static registerTool(instantiationService: IInstantiationService, toolData: IToolData): IDisposable {
|
|
return instantiationService.invokeFunction(accessor => {
|
|
const toolService = accessor.get(ILanguageModelToolsService);
|
|
|
|
const tool = instantiationService.createInstance(SetupTool);
|
|
return toolService.registerTool(toolData, tool);
|
|
});
|
|
}
|
|
|
|
async invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise<IToolResult> {
|
|
const result: IToolResult = {
|
|
content: [
|
|
{
|
|
kind: 'text',
|
|
value: ''
|
|
}
|
|
]
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
async prepareToolInvocation?(parameters: unknown, token: CancellationToken): Promise<IPreparedToolInvocation | undefined> {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export class AINewSymbolNamesProvider {
|
|
|
|
static registerProvider(instantiationService: IInstantiationService, context: ChatEntitlementContext, controller: Lazy<ChatSetupController>): IDisposable {
|
|
return instantiationService.invokeFunction(accessor => {
|
|
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
|
|
|
|
const provider = instantiationService.createInstance(AINewSymbolNamesProvider, context, controller);
|
|
return languageFeaturesService.newSymbolNamesProvider.register('*', provider);
|
|
});
|
|
}
|
|
|
|
constructor(
|
|
private readonly context: ChatEntitlementContext,
|
|
private readonly controller: Lazy<ChatSetupController>,
|
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
|
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
|
|
) {
|
|
}
|
|
|
|
async provideNewSymbolNames(model: ITextModel, range: IRange, triggerKind: NewSymbolNameTriggerKind, token: CancellationToken): Promise<NewSymbolName[] | undefined> {
|
|
await this.instantiationService.invokeFunction(accessor => {
|
|
return ChatSetup.getInstance(this.instantiationService, this.context, this.controller).run({
|
|
forceAnonymous: this.chatEntitlementService.anonymous ? ChatSetupAnonymous.EnabledWithDialog : undefined
|
|
});
|
|
});
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export class ChatCodeActionsProvider {
|
|
|
|
static registerProvider(instantiationService: IInstantiationService): IDisposable {
|
|
return instantiationService.invokeFunction(accessor => {
|
|
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
|
|
|
|
const provider = instantiationService.createInstance(ChatCodeActionsProvider);
|
|
return languageFeaturesService.codeActionProvider.register('*', provider);
|
|
});
|
|
}
|
|
|
|
constructor(
|
|
@IMarkerService private readonly markerService: IMarkerService,
|
|
) {
|
|
}
|
|
|
|
async provideCodeActions(model: ITextModel, range: Range | Selection): Promise<CodeActionList | undefined> {
|
|
const actions: CodeAction[] = [];
|
|
|
|
// "Generate" if the line is whitespace only
|
|
// "Modify" if there is a selection
|
|
let generateOrModifyTitle: string | undefined;
|
|
let generateOrModifyCommand: Command | undefined;
|
|
if (range.isEmpty()) {
|
|
const textAtLine = model.getLineContent(range.startLineNumber);
|
|
if (/^\s*$/.test(textAtLine)) {
|
|
generateOrModifyTitle = localize('generate', "Generate");
|
|
generateOrModifyCommand = AICodeActionsHelper.generate(range);
|
|
}
|
|
} else {
|
|
const textInSelection = model.getValueInRange(range);
|
|
if (!/^\s*$/.test(textInSelection)) {
|
|
generateOrModifyTitle = localize('modify', "Modify");
|
|
generateOrModifyCommand = AICodeActionsHelper.modify(range);
|
|
}
|
|
}
|
|
|
|
if (generateOrModifyTitle && generateOrModifyCommand) {
|
|
actions.push({
|
|
kind: CodeActionKind.RefactorRewrite.append('copilot').value,
|
|
isAI: true,
|
|
title: generateOrModifyTitle,
|
|
command: generateOrModifyCommand,
|
|
});
|
|
}
|
|
|
|
const markers = AICodeActionsHelper.warningOrErrorMarkersAtRange(this.markerService, model.uri, range);
|
|
if (markers.length > 0) {
|
|
|
|
// "Fix" if there are diagnostics in the range
|
|
actions.push({
|
|
kind: CodeActionKind.QuickFix.append('copilot').value,
|
|
isAI: true,
|
|
diagnostics: markers,
|
|
title: localize('fix', "Fix"),
|
|
command: AICodeActionsHelper.fixMarkers(markers, range)
|
|
});
|
|
|
|
// "Explain" if there are diagnostics in the range
|
|
actions.push({
|
|
kind: CodeActionKind.QuickFix.append('explain').append('copilot').value,
|
|
isAI: true,
|
|
diagnostics: markers,
|
|
title: localize('explain', "Explain"),
|
|
command: AICodeActionsHelper.explainMarkers(markers)
|
|
});
|
|
}
|
|
|
|
return {
|
|
actions,
|
|
dispose() { }
|
|
};
|
|
}
|
|
}
|
|
|
|
export class AICodeActionsHelper {
|
|
|
|
static warningOrErrorMarkersAtRange(markerService: IMarkerService, resource: URI, range: Range | Selection): IMarker[] {
|
|
return markerService
|
|
.read({ resource, severities: MarkerSeverity.Error | MarkerSeverity.Warning })
|
|
.filter(marker => range.startLineNumber <= marker.endLineNumber && range.endLineNumber >= marker.startLineNumber);
|
|
}
|
|
|
|
static modify(range: Range): Command {
|
|
return {
|
|
id: INLINE_CHAT_START,
|
|
title: localize('modify', "Modify"),
|
|
arguments: [
|
|
{
|
|
initialSelection: this.rangeToSelection(range),
|
|
initialRange: range,
|
|
position: range.getStartPosition()
|
|
} satisfies { initialSelection: ISelection; initialRange: IRange; position: IPosition }
|
|
]
|
|
};
|
|
}
|
|
|
|
static generate(range: Range): Command {
|
|
return {
|
|
id: INLINE_CHAT_START,
|
|
title: localize('generate', "Generate"),
|
|
arguments: [
|
|
{
|
|
initialSelection: this.rangeToSelection(range),
|
|
initialRange: range,
|
|
position: range.getStartPosition()
|
|
} satisfies { initialSelection: ISelection; initialRange: IRange; position: IPosition }
|
|
]
|
|
};
|
|
}
|
|
|
|
private static rangeToSelection(range: Range): ISelection {
|
|
return new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
|
|
}
|
|
|
|
static explainMarkers(markers: IMarker[]): Command {
|
|
return {
|
|
id: CHAT_OPEN_ACTION_ID,
|
|
title: localize('explain', "Explain"),
|
|
arguments: [
|
|
{
|
|
query: `@workspace /explain ${markers.map(marker => marker.message).join(', ')}`,
|
|
isPartialQuery: true
|
|
} satisfies { query: string; isPartialQuery: boolean }
|
|
]
|
|
};
|
|
}
|
|
|
|
static fixMarkers(markers: IMarker[], range: Range): Command {
|
|
return {
|
|
id: INLINE_CHAT_START,
|
|
title: localize('fix', "Fix"),
|
|
arguments: [
|
|
{
|
|
message: `/fix ${markers.map(marker => marker.message).join(', ')}`,
|
|
initialSelection: this.rangeToSelection(range),
|
|
initialRange: range,
|
|
position: range.getStartPosition()
|
|
} satisfies { message: string; initialSelection: ISelection; initialRange: IRange; position: IPosition }
|
|
]
|
|
};
|
|
}
|
|
}
|