Path: blob/main/extensions/copilot/src/extension/inlineChat2/node/progressMessages.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';6import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';7import { IEnvService } from '../../../platform/env/common/envService';8import { ILogService } from '../../../platform/log/common/logService';9import { CancellationToken } from '../../../util/vs/base/common/cancellation';10import { basename } from '../../../util/vs/base/common/resources';11import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';12import { IDocumentContext } from '../../prompt/node/documentContext';13import { renderPromptElement } from '../../prompts/node/base/promptRenderer';14import { ContextualProgressMessagePrompt, ContextualProgressMessagePromptProps, ProgressMessageScenario, ProgressMessagesPrompt, ProgressMessagesPromptProps } from './progressMessagesPrompt';1516const MESSAGES_PER_FETCH = 10;17const REFETCH_THRESHOLD = 3;1819interface MessageCache {20readonly messages: string[];21readonly fetchInProgress: boolean;22}2324/**25* Provides catchy progress messages for inline chat operations.26* Pre-fetches messages and automatically replenishes when running low.27*/28export class InlineChatProgressMessages {2930private readonly _caches = new Map<ProgressMessageScenario, MessageCache>();31private readonly _pendingFetches = new Map<ProgressMessageScenario, Promise<void>>();3233constructor(34@ILogService private readonly _logService: ILogService,35@IEndpointProvider private readonly _endpointProvider: IEndpointProvider,36@IInstantiationService private readonly _instantiationService: IInstantiationService,37@IEnvService private readonly _envService: IEnvService,38) {39// Initialize caches with fallback messages40this._caches.set('generate', { messages: [...InlineChatProgressMessages._FALLBACK_GENERATE], fetchInProgress: false });41this._caches.set('edit', { messages: [...InlineChatProgressMessages._FALLBACK_EDIT], fetchInProgress: false });4243this.prewarm();44}4546private static readonly _FALLBACK_GENERATE: readonly string[] = [47'Working...',48];4950private static readonly _FALLBACK_EDIT: readonly string[] = [51'Working...',52];5354/**55* Gets the next progress message for the given scenario.56* Automatically triggers a background fetch when running low on messages.57*/58getNextMessage(scenario: ProgressMessageScenario): string {59const cache = this._caches.get(scenario);60if (!cache || cache.messages.length === 0) {61// Should never happen, but use fallback62const fallbacks = scenario === 'generate'63? InlineChatProgressMessages._FALLBACK_GENERATE64: InlineChatProgressMessages._FALLBACK_EDIT;65return fallbacks[Math.floor(Math.random() * fallbacks.length)];66}6768// Get a random message and remove it from the cache69const index = Math.floor(Math.random() * cache.messages.length);70const message = cache.messages[index];71const newMessages = [...cache.messages];72newMessages.splice(index, 1);7374this._caches.set(scenario, { messages: newMessages, fetchInProgress: cache.fetchInProgress });7576// Trigger background fetch if running low77if (newMessages.length < REFETCH_THRESHOLD && !cache.fetchInProgress) {78this._triggerBackgroundFetch(scenario);79}8081return message;82}8384/**85* Gets a contextual progress message based on the user's prompt and document context.86* Falls back to generic messages if contextual generation fails or times out.87*/88async getContextualMessage(prompt: string, documentContext: IDocumentContext, token: CancellationToken): Promise<string> {89const scenario: ProgressMessageScenario = documentContext.selection.isEmpty ? 'generate' : 'edit';9091if (this._envService.isSimulation()) {92return this.getNextMessage(scenario);93}9495try {96const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');9798const selectedCode = documentContext.selection.isEmpty99? undefined100: documentContext.document.getText(documentContext.selection);101102const props: ContextualProgressMessagePromptProps = {103prompt,104fileName: basename(documentContext.document.uri),105uri: documentContext.document.uri,106languageId: documentContext.document.languageId,107selectedCode,108};109110const { messages: promptMessages } = await renderPromptElement(111this._instantiationService,112endpoint,113ContextualProgressMessagePrompt,114props115);116117const response = await endpoint.makeChatRequest2({118debugName: 'contextualProgressMessage',119messages: promptMessages,120finishedCb: undefined,121location: ChatLocation.Editor,122userInitiatedRequest: false,123isConversationRequest: false,124}, token);125126if (response.type === ChatFetchResponseType.Success) {127const message = this._parseContextualMessage(response.value);128if (message) {129this._logService.trace(`[InlineChatProgressMessages] Generated contextual message: ${message}`);130return message;131}132}133} catch (err) {134this._logService.trace(`[InlineChatProgressMessages] Contextual message generation failed, using fallback: ${err}`);135}136137// Fall back to generic message138return this.getNextMessage(scenario);139}140141private _parseContextualMessage(responseText: string): string | undefined {142const trimmed = responseText.trim();143// Remove any surrounding quotes if present144const unquoted = trimmed.replace(/^["']|["']$/g, '');145// Validate the message is reasonable length146if (unquoted.length > 0 && unquoted.length < 60) {147return unquoted;148}149return undefined;150}151152/**153* Pre-warms the cache by fetching messages for both scenarios.154* Can be called during extension activation.155*/156prewarm(): void {157this._triggerBackgroundFetch('generate');158this._triggerBackgroundFetch('edit');159}160161private _triggerBackgroundFetch(scenario: ProgressMessageScenario): void {162if (this._pendingFetches.has(scenario)) {163return;164}165166if (this._envService.isSimulation()) {167return;168}169170const currentCache = this._caches.get(scenario);171if (currentCache) {172this._caches.set(scenario, { messages: currentCache.messages, fetchInProgress: true });173}174175const fetchPromise = this._fetchMessages(scenario).finally(() => {176this._pendingFetches.delete(scenario);177const cache = this._caches.get(scenario);178if (cache) {179this._caches.set(scenario, { messages: cache.messages, fetchInProgress: false });180}181});182183this._pendingFetches.set(scenario, fetchPromise);184}185186private async _fetchMessages(scenario: ProgressMessageScenario): Promise<void> {187try {188const endpoint = await this._endpointProvider.getChatEndpoint('copilot-fast');189190const props: ProgressMessagesPromptProps = { scenario, count: MESSAGES_PER_FETCH };191const { messages: promptMessages } = await renderPromptElement(192this._instantiationService,193endpoint,194ProgressMessagesPrompt,195props196);197198const response = await endpoint.makeChatRequest2({199debugName: 'progressMessages',200messages: promptMessages,201finishedCb: undefined,202location: ChatLocation.Editor,203userInitiatedRequest: false,204isConversationRequest: false,205}, CancellationToken.None);206207if (response.type === ChatFetchResponseType.Success) {208const newMessages = this._parseMessages(response.value);209if (newMessages.length > 0) {210const currentCache = this._caches.get(scenario);211const existingMessages = currentCache?.messages ?? [];212this._caches.set(scenario, {213messages: [...existingMessages, ...newMessages],214fetchInProgress: false215});216this._logService.trace(`[InlineChatProgressMessages] Fetched ${newMessages.length} messages for ${scenario}`);217}218} else {219this._logService.warn(`[InlineChatProgressMessages] Failed to fetch messages for ${scenario}: ${response.reason}`);220}221} catch (err) {222this._logService.error(`[InlineChatProgressMessages] Error fetching messages for ${scenario}`, err);223}224}225226private _parseMessages(responseText: string): string[] {227try {228// Try to extract JSON array from the response229const trimmed = responseText.trim();230let jsonStr = trimmed;231232// Handle markdown code blocks233const jsonMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);234if (jsonMatch) {235jsonStr = jsonMatch[1].trim();236}237238const parsed = JSON.parse(jsonStr);239if (Array.isArray(parsed) && parsed.every(item => typeof item === 'string')) {240return parsed.filter(msg => msg.length > 0 && msg.length < 50);241}242} catch (err) {243this._logService.error('[InlineChatProgressMessages] Failed to parse response as JSON', err);244}245246return [];247}248}249250251