Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
5275 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 './media/chat.css';6import './media/chatAgentHover.css';7import './media/chatViewWelcome.css';8import * as dom from '../../../../../base/browser/dom.js';9import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js';10import { disposableTimeout, timeout } from '../../../../../base/common/async.js';11import { CancellationToken } from '../../../../../base/common/cancellation.js';12import { Codicon } from '../../../../../base/common/codicons.js';13import { toErrorMessage } from '../../../../../base/common/errorMessage.js';14import { Emitter, Event } from '../../../../../base/common/event.js';15import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';16import { Iterable } from '../../../../../base/common/iterator.js';17import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js';18import { ResourceSet } from '../../../../../base/common/map.js';19import { Schemas } from '../../../../../base/common/network.js';20import { filter } from '../../../../../base/common/objects.js';21import { autorun, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';22import { basename, extUri, isEqual } from '../../../../../base/common/resources.js';23import { MicrotaskDelay } from '../../../../../base/common/symbols.js';24import { isDefined } from '../../../../../base/common/types.js';25import { URI } from '../../../../../base/common/uri.js';26import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';27import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';28import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';29import { localize } from '../../../../../nls.js';30import { MenuId } from '../../../../../platform/actions/common/actions.js';31import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';32import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';33import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';3435import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js';36import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';37import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';38import { ILogService } from '../../../../../platform/log/common/log.js';39import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js';40import product from '../../../../../platform/product/common/product.js';41import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';42import { IThemeService } from '../../../../../platform/theme/common/themeService.js';43import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';44import { EditorResourceAccessor } from '../../../../common/editor.js';45import { IEditorService } from '../../../../services/editor/common/editorService.js';46import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';47import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';48import { checkModeOption } from '../../common/chat.js';49import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';50import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';51import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';52import { IChatLayoutService } from '../../common/widget/chatLayoutService.js';53import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../common/model/chatModel.js';54import { ChatMode, IChatModeService } from '../../common/chatModes.js';55import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js';56import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';57import { ChatRequestQueueKind, ChatSendResult, IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js';58import { IChatSessionsService } from '../../common/chatSessionsService.js';59import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js';60import { IChatTodoListService } from '../../common/tools/chatTodoListService.js';61import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';62import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';63import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js';64import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';65import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js';66import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js';67import { PromptsConfig } from '../../common/promptSyntax/config/config.js';68import { IHandOff, PromptHeader } from '../../common/promptSyntax/promptFileParser.js';69import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';70import { handleModeSwitch } from '../actions/chatActions.js';71import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js';72import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js';73import { IChatAttachmentResolveService } from '../attachments/chatAttachmentResolveService.js';74import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js';75import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/chatInputPart.js';76import { IChatListItemTemplate } from './chatListRenderer.js';77import { ChatListWidget } from './chatListWidget.js';78import { ChatEditorOptions } from './chatOptions.js';79import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js';80import { IChatTipService } from '../chatTipService.js';81import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js';82import { ChatContentMarkdownRenderer } from './chatContentMarkdownRenderer.js';83import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';8485const $ = dom.$;8687export interface IChatWidgetStyles extends IChatInputStyles {88readonly inputEditorBackground: string;89readonly resultEditorBackground: string;90}9192export interface IChatWidgetContrib extends IDisposable {9394readonly id: string;9596/**97* A piece of state which is related to the input editor of the chat widget.98* Takes in the `contrib` object that will be saved in the {@link IChatModelInputState}.99*/100getInputState?(contrib: Record<string, unknown>): void;101102/**103* Called with the result of getInputState when navigating input history.104*/105setInputState?(contrib: Readonly<Record<string, unknown>>): void;106}107108interface IChatRequestInputOptions {109input: string;110attachedContext: ChatRequestVariableSet;111}112113export interface IChatWidgetLocationOptions {114location: ChatAgentLocation;115116resolveData?(): IChatLocationData | undefined;117}118119export function isQuickChat(widget: IChatWidget): boolean {120return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isQuickChat);121}122123function isInlineChat(widget: IChatWidget): boolean {124return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isInlineChat);125}126127type ChatHandoffClickEvent = {128fromAgent: string;129toAgent: string;130hasPrompt: boolean;131autoSend: boolean;132};133134type ChatHandoffClickClassification = {135owner: 'digitarald';136comment: 'Event fired when a user clicks on a handoff prompt in the chat suggest-next widget';137fromAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode the user was in before clicking the handoff' };138toAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode specified in the handoff' };139hasPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff includes a prompt' };140autoSend: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff automatically submits the request' };141};142143type ChatHandoffWidgetShownEvent = {144agent: string;145handoffCount: number;146};147148type ChatHandoffWidgetShownClassification = {149owner: 'digitarald';150comment: 'Event fired when the suggest-next widget is shown with handoff prompts';151agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current agent/mode that has handoffs defined' };152handoffCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of handoff options shown to the user' };153};154155const supportsAllAttachments: Required<IChatAgentAttachmentCapabilities> = {156supportsFileAttachments: true,157supportsToolAttachments: true,158supportsMCPAttachments: true,159supportsImageAttachments: true,160supportsSearchResultAttachments: true,161supportsInstructionAttachments: true,162supportsSourceControlAttachments: true,163supportsProblemAttachments: true,164supportsSymbolAttachments: true,165supportsTerminalAttachments: true,166};167168const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate.");169170export class ChatWidget extends Disposable implements IChatWidget {171172// eslint-disable-next-line @typescript-eslint/no-explicit-any173static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = [];174175private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());176readonly onDidSubmitAgent = this._onDidSubmitAgent.event;177178private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());179readonly onDidChangeAgent = this._onDidChangeAgent.event;180181private _onDidFocus = this._register(new Emitter<void>());182readonly onDidFocus = this._onDidFocus.event;183184private _onDidChangeViewModel = this._register(new Emitter<IChatWidgetViewModelChangeEvent>());185readonly onDidChangeViewModel = this._onDidChangeViewModel.event;186187private _onDidScroll = this._register(new Emitter<void>());188readonly onDidScroll = this._onDidScroll.event;189190private _onDidAcceptInput = this._register(new Emitter<void>());191readonly onDidAcceptInput = this._onDidAcceptInput.event;192193private _onDidHide = this._register(new Emitter<void>());194readonly onDidHide = this._onDidHide.event;195196private _onDidShow = this._register(new Emitter<void>());197readonly onDidShow = this._onDidShow.event;198199private _onDidChangeParsedInput = this._register(new Emitter<void>());200readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event;201202private readonly _onWillMaybeChangeHeight = this._register(new Emitter<void>());203readonly onWillMaybeChangeHeight: Event<void> = this._onWillMaybeChangeHeight.event;204205private _onDidChangeHeight = this._register(new Emitter<number>());206readonly onDidChangeHeight = this._onDidChangeHeight.event;207208private readonly _onDidChangeContentHeight = this._register(new Emitter<void>());209readonly onDidChangeContentHeight: Event<void> = this._onDidChangeContentHeight.event;210211private _onDidChangeEmptyState = this._register(new Emitter<void>());212readonly onDidChangeEmptyState = this._onDidChangeEmptyState.event;213214contribs: ReadonlyArray<IChatWidgetContrib> = [];215216private listContainer!: HTMLElement;217private container!: HTMLElement;218219get domNode() { return this.container; }220221private listWidget!: ChatListWidget;222private readonly _codeBlockModelCollection: CodeBlockModelCollection;223224private readonly visibilityTimeoutDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());225private readonly visibilityAnimationFrameDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());226227private readonly inputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());228private readonly inlineInputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());229private inputContainer!: HTMLElement;230private focusedInputDOM!: HTMLElement;231private editorOptions!: ChatEditorOptions;232233private recentlyRestoredCheckpoint: boolean = false;234235private welcomeMessageContainer!: HTMLElement;236private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());237238private gettingStartedTipContainer: HTMLElement | undefined;239private readonly _gettingStartedTipPart = this._register(new MutableDisposable<DisposableStore>());240241private readonly chatSuggestNextWidget: ChatSuggestNextWidget;242243private bodyDimension: dom.Dimension | undefined;244private visibleChangeCount = 0;245private requestInProgress: IContextKey<boolean>;246private agentInInput: IContextKey<boolean>;247248private _visible = false;249get visible() { return this._visible; }250251private _instructionFilesCheckPromise: Promise<boolean> | undefined;252private _instructionFilesExist: boolean | undefined;253254private _isRenderingWelcome = false;255256// Coding agent locking state257private _lockedAgent?: {258id: string;259name: string;260prefix: string;261displayName: string;262};263private readonly _lockedToCodingAgentContextKey: IContextKey<boolean>;264private readonly _agentSupportsAttachmentsContextKey: IContextKey<boolean>;265private readonly _sessionIsEmptyContextKey: IContextKey<boolean>;266private readonly _hasPendingRequestsContextKey: IContextKey<boolean>;267private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments;268269// Cache for prompt file descriptions to avoid async calls during rendering270private readonly promptDescriptionsCache = new Map<string, string>();271private readonly promptUriCache = new Map<string, URI>();272private _isLoadingPromptDescriptions = false;273274private readonly viewModelDisposables = this._register(new DisposableStore());275private _viewModel: ChatViewModel | undefined;276277private set viewModel(viewModel: ChatViewModel | undefined) {278if (this._viewModel === viewModel) {279return;280}281282const previousSessionResource = this._viewModel?.sessionResource;283this.viewModelDisposables.clear();284285this._viewModel = viewModel;286if (viewModel) {287this.viewModelDisposables.add(viewModel);288this.logService.debug('ChatWidget#setViewModel: have viewModel');289290// If switching to a model with a request in progress, play progress sound291if (viewModel.model.requestInProgress.get()) {292this.chatAccessibilityService.acceptRequest(viewModel.sessionResource, true);293}294} else {295this.logService.debug('ChatWidget#setViewModel: no viewModel');296}297298this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource });299}300301get viewModel() {302return this._viewModel;303}304305private readonly _editingSession = observableValue<IChatEditingSession | undefined>(this, undefined);306307private parsedChatRequest: IParsedChatRequest | undefined;308get parsedInput() {309if (this.parsedChatRequest === undefined) {310if (!this.viewModel) {311return { text: '', parts: [] };312}313314this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser)315.parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, {316selectedAgent: this._lastSelectedAgent,317mode: this.input.currentModeKind,318forcedAgent: this._lockedAgent?.id ? this.chatAgentService.getAgent(this._lockedAgent.id) : undefined319});320this._onDidChangeParsedInput.fire();321}322323return this.parsedChatRequest;324}325326get scopedContextKeyService(): IContextKeyService {327return this.contextKeyService;328}329330private readonly _location: IChatWidgetLocationOptions;331get location() {332return this._location.location;333}334335readonly viewContext: IChatWidgetViewContext;336337get supportsChangingModes(): boolean {338return !!this.viewOptions.supportsChangingModes;339}340341get locationData() {342return this._location.resolveData?.();343}344345constructor(346location: ChatAgentLocation | IChatWidgetLocationOptions,347viewContext: IChatWidgetViewContext | undefined,348private readonly viewOptions: IChatWidgetViewOptions,349private readonly styles: IChatWidgetStyles,350@ICodeEditorService private readonly codeEditorService: ICodeEditorService,351@IEditorService private readonly editorService: IEditorService,352@IConfigurationService private readonly configurationService: IConfigurationService,353@IDialogService private readonly dialogService: IDialogService,354@IContextKeyService private readonly contextKeyService: IContextKeyService,355@IInstantiationService private readonly instantiationService: IInstantiationService,356@IChatService private readonly chatService: IChatService,357@IChatAgentService private readonly chatAgentService: IChatAgentService,358@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,359@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService,360@ILogService private readonly logService: ILogService,361@IThemeService private readonly themeService: IThemeService,362@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,363@IChatEditingService chatEditingService: IChatEditingService,364@ITelemetryService private readonly telemetryService: ITelemetryService,365@IPromptsService private readonly promptsService: IPromptsService,366@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,367@IChatModeService private readonly chatModeService: IChatModeService,368@IChatLayoutService private readonly chatLayoutService: IChatLayoutService,369@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,370@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,371@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,372@IChatTodoListService private readonly chatTodoListService: IChatTodoListService,373@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,374@ILifecycleService private readonly lifecycleService: ILifecycleService,375@IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService,376@IChatTipService private readonly chatTipService: IChatTipService,377) {378super();379380this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);381this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService);382this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService);383this._hasPendingRequestsContextKey = ChatContextKeys.hasPendingRequests.bindTo(this.contextKeyService);384385this.viewContext = viewContext ?? {};386387const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel);388389if (typeof location === 'object') {390this._location = location;391} else {392this._location = { location };393}394395ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true);396ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location);397ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this));398this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService);399this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService);400401this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded()));402403this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => {404const currentSession = this._editingSession.read(reader);405if (!currentSession) {406return;407}408const entries = currentSession.entries.read(reader);409const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified);410return decidedEntries.map(entry => entry.entryId);411}));412this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => {413const currentSession = this._editingSession.read(reader);414const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here415const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified);416return decidedEntries.length > 0;417}));418this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => {419const currentSession = this._editingSession.read(reader);420if (!currentSession) {421return false;422}423const entries = currentSession.entries.read(reader);424return entries.length > 0;425}));426this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => {427return this._editingSession.read(reader) !== null;428}));429this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => {430return this._editingSession.read(r)?.canUndo.read(r) || false;431}));432this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => {433return this._editingSession.read(r)?.canRedo.read(r) || false;434}));435this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => {436const chatModel = viewModelObs.read(r)?.model;437const editingSession = this._editingSession.read(r);438if (!editingSession || !chatModel) {439return false;440}441const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r);442return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete;443}));444445this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection, undefined));446this.chatSuggestNextWidget = this._register(this.instantiationService.createInstance(ChatSuggestNextWidget));447448this._register(autorun(r => {449const viewModel = viewModelObs.read(r);450const sessions = chatEditingService.editingSessionsObs.read(r);451452const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, viewModel?.sessionResource));453this._editingSession.set(undefined, undefined);454this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc.455456if (!session) {457// none or for a different chat widget458return;459}460461const entries = session.entries.read(r);462for (const entry of entries) {463entry.state.read(r); // SIGNAL464}465466this._editingSession.set(session, undefined);467468r.store.add(session.onDidDispose(() => {469this._editingSession.set(undefined, undefined);470this.renderChatEditingSessionState();471}));472r.store.add(this.inputEditor.onDidChangeModelContent(() => {473if (this.getInput() === '') {474this.refreshParsedInput();475}476}));477this.renderChatEditingSessionState();478}));479480this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise<ICodeEditor | null> => {481const resource = input.resource;482if (resource.scheme !== Schemas.vscodeChatCodeBlock) {483return null;484}485486const responseId = resource.path.split('/').at(1);487if (!responseId) {488return null;489}490491const item = this.viewModel?.getItems().find(item => item.id === responseId);492if (!item) {493return null;494}495496// TODO: needs to reveal the chat view497498this.reveal(item);499500await timeout(0); // wait for list to actually render501502for (const codeBlockPart of this.listWidget.editorsInUse()) {503if (extUri.isEqual(codeBlockPart.uri, resource, true)) {504const editor = codeBlockPart.editor;505506let relativeTop = 0;507const editorDomNode = editor.getDomNode();508if (editorDomNode) {509const row = dom.findParentWithClass(editorDomNode, 'monaco-list-row');510if (row) {511relativeTop = dom.getTopLeftOffset(editorDomNode).top - dom.getTopLeftOffset(row).top;512}513}514515if (input.options?.selection) {516const editorSelectionTopOffset = editor.getTopForPosition(input.options.selection.startLineNumber, input.options.selection.startColumn);517relativeTop += editorSelectionTopOffset;518519editor.focus();520editor.setSelection({521startLineNumber: input.options.selection.startLineNumber,522startColumn: input.options.selection.startColumn,523endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber,524endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn525});526}527528this.reveal(item, relativeTop);529530return editor;531}532}533return null;534}));535536this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext()));537538this._register(this.chatTodoListService.onDidUpdateTodos((sessionResource) => {539if (isEqual(this.viewModel?.sessionResource, sessionResource)) {540this.inputPart.renderChatTodoListWidget(sessionResource);541}542}));543}544545private _lastSelectedAgent: IChatAgentData | undefined;546set lastSelectedAgent(agent: IChatAgentData | undefined) {547this.parsedChatRequest = undefined;548this._lastSelectedAgent = agent;549this._updateAgentCapabilitiesContextKeys(agent);550this._onDidChangeParsedInput.fire();551}552553get lastSelectedAgent(): IChatAgentData | undefined {554return this._lastSelectedAgent;555}556557private _updateAgentCapabilitiesContextKeys(agent: IChatAgentData | undefined): void {558// Check if the agent has capabilities defined directly559const capabilities = agent?.capabilities ?? (this._lockedAgent ? this.chatSessionsService.getCapabilitiesForSessionType(this._lockedAgent.id) : undefined);560this._attachmentCapabilities = capabilities ?? supportsAllAttachments;561562const supportsAttachments = Object.keys(filter(this._attachmentCapabilities, (key, value) => value === true)).length > 0;563this._agentSupportsAttachmentsContextKey.set(supportsAttachments);564}565566get supportsFileReferences(): boolean {567return !!this.viewOptions.supportsFileReferences;568}569570get attachmentCapabilities(): IChatAgentAttachmentCapabilities {571return this._attachmentCapabilities;572}573574/**575* Either the inline input (when editing) or the main input part576*/577get input(): ChatInputPart {578return this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart;579}580581/**582* The main input part at the buttom of the chat widget. Use `input` to get the active input (main or inline editing part).583*/584get inputPart(): ChatInputPart {585return this.inputPartDisposable.value!;586}587588private get inlineInputPart(): ChatInputPart {589return this.inlineInputPartDisposable.value!;590}591592get inputEditor(): ICodeEditor {593return this.input.inputEditor;594}595596get contentHeight(): number {597return this.input.height.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height;598}599600get attachmentModel(): ChatAttachmentModel {601return this.input.attachmentModel;602}603604render(parent: HTMLElement): void {605const viewId = isIChatViewViewContext(this.viewContext) ? this.viewContext.viewId : undefined;606this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground));607const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false;608const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop;609const renderStyle = this.viewOptions.renderStyle;610const renderInputToolbarBelowInput = this.viewOptions.renderInputToolbarBelowInput ?? false;611612this.container = dom.append(parent, $('.interactive-session'));613this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' }));614this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput()));615616this._register(this.chatSuggestNextWidget.onDidChangeHeight(() => {617if (this.bodyDimension) {618this.layout(this.bodyDimension.height, this.bodyDimension.width);619}620}));621this._register(this.chatSuggestNextWidget.onDidSelectPrompt(({ handoff, agentId }) => {622this.handleNextPromptSelection(handoff, agentId);623}));624625if (renderInputOnTop) {626this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });627this.listContainer = dom.append(this.container, $(`.interactive-list`));628} else {629this.listContainer = dom.append(this.container, $(`.interactive-list`));630dom.append(this.container, this.chatSuggestNextWidget.domNode);631this.gettingStartedTipContainer = dom.append(this.container, $('.chat-getting-started-tip-container', { style: 'display: none' }));632this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });633}634635this.renderWelcomeViewContentIfNeeded();636this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle });637638// Update the font family and size639this._register(autorun(reader => {640const fontFamily = this.chatLayoutService.fontFamily.read(reader);641const fontSize = this.chatLayoutService.fontSize.read(reader);642643this.container.style.setProperty('--vscode-chat-font-family', fontFamily);644this.container.style.fontSize = `${fontSize}px`;645646if (this.visible) {647this.listWidget.rerender();648}649}));650651this._register(Event.runAndSubscribe(this.editorOptions.onDidChange, () => this.onDidStyleChange()));652653// Do initial render654if (this.viewModel) {655this.onDidChangeItems();656this.listWidget.scrollToEnd();657}658659this.contribs = ChatWidget.CONTRIBS.map(contrib => {660try {661return this._register(this.instantiationService.createInstance(contrib, this));662} catch (err) {663this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err));664return undefined;665}666}).filter(isDefined);667668this._register(this.chatWidgetService.register(this));669670const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput);671this._register(autorun(r => {672const input = parsedInput.read(r);673674const newPromptAttachments = new Map<string, IChatRequestVariableEntry>();675const oldPromptAttachments = new Set<string>();676677// get all attachments, know those that are prompt-referenced678for (const attachment of this.attachmentModel.attachments) {679if (attachment.range) {680oldPromptAttachments.add(attachment.id);681}682}683684// update/insert prompt-referenced attachments685for (const part of input.parts) {686if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) {687const entry = part.toVariableEntry();688newPromptAttachments.set(entry.id, entry);689oldPromptAttachments.delete(entry.id);690}691}692693this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values());694}));695696if (!this.focusedInputDOM) {697this.focusedInputDOM = this.container.appendChild(dom.$('.focused-input-dom'));698}699}700701focusInput(): void {702this.input.focus();703704// Sometimes focusing the input part is not possible,705// but we'd like to be the last focused chat widget,706// so we emit an optimistic onDidFocus event nonetheless.707this._onDidFocus.fire();708}709710focusTodosView(): boolean {711if (!this.input.hasVisibleTodos()) {712return false;713}714715return this.input.focusTodoList();716}717718toggleTodosViewFocus(): boolean {719if (!this.input.hasVisibleTodos()) {720return false;721}722723if (this.input.isTodoListFocused()) {724this.focusInput();725return true;726}727728return this.input.focusTodoList();729}730731focusQuestionCarousel(): boolean {732if (!this.input.questionCarousel) {733return false;734}735736return this.input.focusQuestionCarousel();737}738739toggleQuestionCarouselFocus(): boolean {740if (!this.input.questionCarousel) {741return false;742}743744if (this.input.isQuestionCarouselFocused()) {745this.focusInput();746return true;747}748749return this.input.focusQuestionCarousel();750}751752toggleTipFocus(): boolean {753if (this.listWidget.hasTipFocus()) {754this.focusInput();755return true;756}757758return this.listWidget.focusTip();759}760761hasInputFocus(): boolean {762return this.input.hasFocus();763}764765refreshParsedInput() {766if (!this.viewModel) {767return;768}769770const previous = this.parsedChatRequest;771this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind });772if (!previous || !IParsedChatRequest.equals(previous, this.parsedChatRequest)) {773this._onDidChangeParsedInput.fire();774}775}776777getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined {778if (!isResponseVM(item)) {779return;780}781const items = this.viewModel?.getItems();782if (!items) {783return;784}785const responseItems = items.filter(i => isResponseVM(i));786const targetIndex = responseItems.indexOf(item);787if (targetIndex === undefined) {788return;789}790const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1;791if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) {792return;793}794return responseItems[indexToFocus];795}796797async clear(): Promise<void> {798this.logService.debug('ChatWidget#clear');799if (this._dynamicMessageLayoutData) {800this._dynamicMessageLayoutData.enabled = true;801}802803if (this.viewModel?.editing) {804this.finishedEditing();805}806807if (this.viewModel) {808this.viewModel.resetInputPlaceholder();809}810if (this._lockedAgent) {811this.lockToCodingAgent(this._lockedAgent.name, this._lockedAgent.displayName, this._lockedAgent.id);812} else {813this.unlockFromCodingAgent();814}815816this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);817this.chatSuggestNextWidget.hide();818await this.viewOptions.clear?.();819}820821private onDidChangeItems(skipDynamicLayout?: boolean) {822if (this._visible || !this.viewModel) {823const items = this.viewModel?.getItems() ?? [];824825if (items.length > 0) {826this.updateChatViewVisibility();827} else {828this.renderWelcomeViewContentIfNeeded();829}830831this._onWillMaybeChangeHeight.fire();832833// Update list widget state and refresh834this.listWidget.setVisibleChangeCount(this.visibleChangeCount);835this.listWidget.refresh();836837if (!skipDynamicLayout && this._dynamicMessageLayoutData) {838this.layoutDynamicChatTreeItemMode();839}840841this.renderFollowups();842}843}844845/**846* Updates the DOM visibility of welcome view and chat list immediately847*/848private updateChatViewVisibility(): void {849if (this.viewModel) {850const isStandardLayout = this.viewOptions.renderStyle !== 'compact' && this.viewOptions.renderStyle !== 'minimal';851const numItems = this.viewModel.getItems().length;852dom.setVisibility(numItems === 0, this.welcomeMessageContainer);853dom.setVisibility(numItems !== 0, this.listContainer);854855// Show/hide the getting-started tip container based on empty state.856// Only use this in the standard chat layout where the welcome view is shown.857if (isStandardLayout && this.gettingStartedTipContainer) {858if (numItems === 0) {859this.renderGettingStartedTipIfNeeded();860this.container.classList.toggle('chat-has-getting-started-tip', !!this._gettingStartedTipPart.value);861} else {862// Dispose the cached tip part so the next empty state picks a863// fresh (rotated) tip instead of re-showing the stale one.864this._gettingStartedTipPart.clear();865dom.clearNode(this.gettingStartedTipContainer);866// Reset inline positioning from layoutGettingStartedTipPosition867// so the next render starts from the CSS defaults.868this.gettingStartedTipContainer.style.top = '';869this.gettingStartedTipContainer.style.bottom = '';870this.gettingStartedTipContainer.style.left = '';871this.gettingStartedTipContainer.style.right = '';872this.gettingStartedTipContainer.style.width = '';873dom.setVisibility(false, this.gettingStartedTipContainer);874this.container.classList.toggle('chat-has-getting-started-tip', false);875}876}877}878879// Only show welcome getting started until extension is installed880this.container.classList.toggle('chat-view-getting-started-disabled', this.chatEntitlementService.sentiment.installed);881882this._onDidChangeEmptyState.fire();883}884885isEmpty(): boolean {886return (this.viewModel?.getItems().length ?? 0) === 0;887}888889/**890* Renders the welcome view content when needed.891*/892private renderWelcomeViewContentIfNeeded() {893if (this._isRenderingWelcome) {894return;895}896897this._isRenderingWelcome = true;898try {899if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal' || this.lifecycleService.willShutdown) {900return;901}902903const numItems = this.viewModel?.getItems().length ?? 0;904if (!numItems) {905const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind);906let additionalMessage: string | IMarkdownString | undefined;907if (this.chatEntitlementService.anonymous && !this.chatEntitlementService.sentiment.installed) {908const providers = product.defaultChatAgent.provider;909additionalMessage = new MarkdownString(localize({ key: 'settings', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3}).", providers.default.name, providers.default.name, product.defaultChatAgent.termsStatementUrl, product.defaultChatAgent.privacyStatementUrl), { isTrusted: true });910} else {911additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage;912}913if (!additionalMessage && !this._lockedAgent) {914additionalMessage = this._getGenerateInstructionsMessage();915}916const welcomeContent = this.getWelcomeViewContent(additionalMessage);917if (!this.welcomePart.value || this.welcomePart.value.needsRerender(welcomeContent)) {918dom.clearNode(this.welcomeMessageContainer);919920this.welcomePart.value = this.instantiationService.createInstance(921ChatViewWelcomePart,922welcomeContent,923{924location: this.location,925isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent926}927);928dom.append(this.welcomeMessageContainer, this.welcomePart.value.element);929}930}931932this.updateChatViewVisibility();933} finally {934this._isRenderingWelcome = false;935}936}937938private renderGettingStartedTipIfNeeded(): void {939const tipContainer = this.gettingStartedTipContainer;940if (!tipContainer) {941return;942}943944// Already showing a tip945if (this._gettingStartedTipPart.value) {946dom.setVisibility(true, tipContainer);947return;948}949950const tip = this.chatTipService.getWelcomeTip(this.contextKeyService);951if (!tip) {952dom.setVisibility(false, tipContainer);953return;954}955956const store = new DisposableStore();957const renderer = this.instantiationService.createInstance(ChatContentMarkdownRenderer);958const tipPart = store.add(this.instantiationService.createInstance(ChatTipContentPart,959tip,960renderer,961() => this.chatTipService.getWelcomeTip(this.contextKeyService),962));963tipContainer.appendChild(tipPart.domNode);964965store.add(tipPart.onDidHide(() => {966tipPart.domNode.remove();967this._gettingStartedTipPart.clear();968dom.setVisibility(false, tipContainer);969this.container.classList.toggle('chat-has-getting-started-tip', false);970}));971972this._gettingStartedTipPart.value = store;973dom.setVisibility(true, tipContainer);974975// Best-effort synchronous position (works when layout is already settled,976// e.g. the very first render after page load).977this.layoutGettingStartedTipPosition();978979// Also schedule a deferred correction for cases where the browser980// hasn't finished layout yet (e.g. returning to the welcome view981// after a conversation).982store.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(tipContainer), () => {983this.layoutGettingStartedTipPosition();984}));985}986987private layoutGettingStartedTipPosition(): void {988if (!this.container || !this.gettingStartedTipContainer || !this.inputPart) {989return;990}991992const inputContainer = this.inputPart.inputContainerElement;993if (!inputContainer) {994return;995}996997const containerRect = this.container.getBoundingClientRect();998const inputRect = inputContainer.getBoundingClientRect();999const tipRect = this.gettingStartedTipContainer.getBoundingClientRect();10001001// Align the tip horizontally with the input container.1002const left = inputRect.left - containerRect.left;1003this.gettingStartedTipContainer.style.left = `${left}px`;1004this.gettingStartedTipContainer.style.right = 'auto';1005this.gettingStartedTipContainer.style.width = `${inputRect.width}px`;10061007// Position the tip so its bottom edge sits flush against the input's1008// top edge for a seamless visual connection.1009const topOffset = inputRect.top - containerRect.top - tipRect.height;1010if (topOffset > 0) {1011this.gettingStartedTipContainer.style.top = `${topOffset}px`;1012this.gettingStartedTipContainer.style.bottom = 'auto';1013}1014}10151016private _getGenerateInstructionsMessage(): IMarkdownString {1017// Start checking for instruction files immediately if not already done1018if (!this._instructionFilesCheckPromise) {1019this._instructionFilesCheckPromise = this._checkForAgentInstructionFiles();1020// Use VS Code's idiomatic pattern for disposal-safe promise callbacks1021this._register(thenIfNotDisposed(this._instructionFilesCheckPromise, hasFiles => {1022this._instructionFilesExist = hasFiles;1023// Only re-render if the current view still doesn't have items and we're showing the welcome message1024const hasViewModelItems = this.viewModel?.getItems().length ?? 0;1025if (hasViewModelItems === 0) {1026this.renderWelcomeViewContentIfNeeded();1027}1028}));1029}10301031// If we already know the result, use it1032if (this._instructionFilesExist === true) {1033// Don't show generate instructions message if files exist1034return new MarkdownString('');1035} else if (this._instructionFilesExist === false) {1036// Show generate instructions message if no files exist1037const generateInstructionsCommand = 'workbench.action.chat.generateInstructions';1038return new MarkdownString(localize(1039'chatWidget.instructions',1040"[Generate Agent Instructions]({0}) to onboard AI onto your codebase.",1041`command:${generateInstructionsCommand}`1042), { isTrusted: { enabledCommands: [generateInstructionsCommand] } });1043}10441045// While checking, don't show the generate instructions message1046return new MarkdownString('');1047}10481049/**1050* Checks if any agent instruction files (.github/copilot-instructions.md or AGENTS.md) exist in the workspace.1051* Used to determine whether to show the "Generate Agent Instructions" hint.1052*1053* @returns true if instruction files exist OR if instruction features are disabled (to hide the hint)1054*/1055private async _checkForAgentInstructionFiles(): Promise<boolean> {1056try {1057return (await this.promptsService.listAgentInstructions(CancellationToken.None)).length > 0;1058} catch (error) {1059// On error, assume no instruction files exist to be safe1060this.logService.warn('[ChatWidget] Error checking for instruction files:', error);1061return false;1062}1063}10641065private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent {1066if (this.isLockedToCodingAgent) {1067// Check for provider-specific customizations from chat sessions service1068const providerIcon = this._lockedAgent ? this.chatSessionsService.getIconForSessionType(this._lockedAgent.id) : undefined;1069const providerTitle = this._lockedAgent ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgent.id) : undefined;1070const providerMessage = this._lockedAgent ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgent.id) : undefined;10711072// Fallback to default messages if provider doesn't specify1073const message = providerMessage1074? new MarkdownString(providerMessage)1075: (this._lockedAgent?.prefix === '@copilot '1076? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._lockedAgent.prefix, 'https://aka.ms/coding-agent-docs') + DISCLAIMER, { isTrusted: true })1077: new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._lockedAgent?.prefix) + DISCLAIMER));10781079return {1080title: providerTitle ?? localize('codingAgentTitle', "Delegate to {0}", this._lockedAgent?.prefix),1081message,1082icon: providerIcon ?? Codicon.sendToRemoteAgent,1083additionalMessage,1084useLargeIcon: !!providerIcon,1085};1086}10871088let title: string;1089if (this.input.currentModeKind === ChatModeKind.Ask) {1090title = localize('chatDescription', "Ask about your code");1091} else if (this.input.currentModeKind === ChatModeKind.Edit) {1092title = localize('editsTitle', "Edit in context");1093} else {1094title = localize('agentTitle', "Build with Agent");1095}10961097return {1098title,1099message: new MarkdownString(DISCLAIMER),1100icon: Codicon.chatSparkle,1101additionalMessage,1102suggestedPrompts: this.getPromptFileSuggestions(),1103};1104}11051106private getPromptFileSuggestions(): IChatSuggestedPrompts[] {11071108// Use predefined suggestions for new users1109if (!this.chatEntitlementService.sentiment.installed) {1110const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY;1111if (isEmpty) {1112return [1113{1114icon: Codicon.vscode,1115label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"),1116prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"),1117},1118{1119icon: Codicon.newFolder,1120label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"),1121prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"),1122}1123];1124} else {1125return [1126{1127icon: Codicon.debugAlt,1128label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"),1129prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"),1130},1131{1132icon: Codicon.gear,1133label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"),1134prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"),1135}1136];1137}1138}11391140// Get the current workspace folder context if available1141const activeEditor = this.editorService.activeEditor;1142const resource = activeEditor ? EditorResourceAccessor.getOriginalUri(activeEditor) : undefined;11431144// Get the prompt file suggestions configuration1145const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService, resource);1146if (!suggestions) {1147return [];1148}11491150const result: IChatSuggestedPrompts[] = [];1151const promptsToLoad: string[] = [];11521153// First, collect all prompts that need loading (regardless of shouldInclude)1154for (const [promptName] of Object.entries(suggestions)) {1155const description = this.promptDescriptionsCache.get(promptName);1156if (description === undefined) {1157promptsToLoad.push(promptName);1158}1159}11601161// If we have prompts to load, load them asynchronously and don't return anything yet1162// But only if we're not already loading to prevent infinite loop1163if (promptsToLoad.length > 0 && !this._isLoadingPromptDescriptions) {1164this.loadPromptDescriptions(promptsToLoad);1165return [];1166}11671168// Now process the suggestions with loaded descriptions1169const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = [];11701171for (const [promptName, condition] of Object.entries(suggestions)) {1172let score = 0;11731174// Handle boolean conditions1175if (typeof condition === 'boolean') {1176score = condition ? 1 : 0;1177}1178// Handle when clause conditions1179else if (typeof condition === 'string') {1180try {1181const whenClause = ContextKeyExpr.deserialize(condition);1182if (whenClause) {1183// Test against all open code editors1184const allEditors = this.codeEditorService.listCodeEditors();11851186if (allEditors.length > 0) {1187// Count how many editors match the when clause1188score = allEditors.reduce((count, editor) => {1189try {1190const editorContext = this.contextKeyService.getContext(editor.getDomNode());1191return count + (whenClause.evaluate(editorContext) ? 1 : 0);1192} catch (error) {1193// Log error for this specific editor but continue with others1194this.logService.warn('Failed to evaluate when clause for editor:', error);1195return count;1196}1197}, 0);1198} else {1199// Fallback to global context if no editors are open1200score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0;1201}1202} else {1203score = 0;1204}1205} catch (error) {1206// Log the error but don't fail completely1207this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error);1208score = 0;1209}1210}12111212if (score > 0) {1213promptsWithScores.push({ promptName, condition, score });1214}1215}12161217// Sort by score (descending) and take top 51218promptsWithScores.sort((a, b) => b.score - a.score);1219const topPrompts = promptsWithScores.slice(0, 5);12201221// Build the final result array1222for (const { promptName } of topPrompts) {1223const description = this.promptDescriptionsCache.get(promptName);1224const commandLabel = localize('chatWidget.promptFile.commandLabel', "{0}", promptName);1225const uri = this.promptUriCache.get(promptName);1226const descriptionText = description?.trim() ? description : undefined;1227result.push({1228icon: Codicon.run,1229label: commandLabel,1230description: descriptionText,1231prompt: `/${promptName} `,1232uri: uri1233});1234}12351236return result;1237}12381239private async loadPromptDescriptions(promptNames: string[]): Promise<void> {1240// Don't start loading if the widget is being disposed1241if (this._store.isDisposed) {1242return;1243}12441245// Set loading guard to prevent infinite loop1246this._isLoadingPromptDescriptions = true;1247try {1248// Get all available prompt files with their metadata1249const promptCommands = await this.promptsService.getPromptSlashCommands(CancellationToken.None);12501251let cacheUpdated = false;1252// Load descriptions only for the specified prompts1253for (const promptCommand of promptCommands) {1254if (promptNames.includes(promptCommand.name)) {1255const description = promptCommand.description;1256if (description) {1257this.promptDescriptionsCache.set(promptCommand.name, description);1258cacheUpdated = true;1259} else {1260// Set empty string to indicate we've checked this prompt1261this.promptDescriptionsCache.set(promptCommand.name, '');1262cacheUpdated = true;1263}1264}1265}12661267// Fire event to trigger a re-render of the welcome view only if cache was updated1268if (cacheUpdated) {1269this.renderWelcomeViewContentIfNeeded();1270}1271} catch (error) {1272this.logService.warn('Failed to load specific prompt descriptions:', error);1273} finally {1274// Always clear the loading guard, even on error1275this._isLoadingPromptDescriptions = false;1276}1277}12781279private async renderChatEditingSessionState() {1280if (!this.input) {1281return;1282}1283this.input.renderChatEditingSessionState(this._editingSession.get() ?? null);1284}12851286private async renderFollowups(): Promise<void> {1287const lastItem = this.listWidget.lastItem;1288if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) {1289this.input.renderFollowups(lastItem.replyFollowups, lastItem);1290} else {1291this.input.renderFollowups(undefined, undefined);1292}1293}12941295private renderChatSuggestNextWidget(): void {1296if (this.lifecycleService.willShutdown) {1297return;1298}12991300// Skip rendering in coding agent sessions1301if (this.isLockedToCodingAgent) {1302this.chatSuggestNextWidget.hide();1303return;1304}13051306const items = this.viewModel?.getItems() ?? [];1307if (!items.length) {1308return;1309}13101311const lastItem = items[items.length - 1];1312const lastResponseComplete = lastItem && isResponseVM(lastItem) && lastItem.isComplete;1313if (!lastResponseComplete) {1314return;1315}1316// Get the currently selected mode directly from the observable1317// Note: We use currentModeObs instead of currentModeKind because currentModeKind returns1318// the ChatModeKind enum (e.g., 'agent'), which doesn't distinguish between custom modes.1319// Custom modes all have kind='agent' but different IDs.1320const currentMode = this.input.currentModeObs.get();1321const handoffs = currentMode?.handOffs?.get();13221323// Only show if: mode has handoffs AND chat has content AND not quick chat1324const shouldShow = currentMode && handoffs && handoffs.length > 0;13251326if (shouldShow) {1327// Log telemetry only when widget transitions from hidden to visible1328const wasHidden = this.chatSuggestNextWidget.domNode.style.display === 'none';1329this.chatSuggestNextWidget.render(currentMode);13301331if (wasHidden) {1332this.telemetryService.publicLog2<ChatHandoffWidgetShownEvent, ChatHandoffWidgetShownClassification>('chat.handoffWidgetShown', {1333agent: currentMode.id,1334handoffCount: handoffs.length1335});1336}1337} else {1338this.chatSuggestNextWidget.hide();1339}13401341// Trigger layout update1342if (this.bodyDimension) {1343this.layout(this.bodyDimension.height, this.bodyDimension.width);1344}1345}13461347private handleNextPromptSelection(handoff: IHandOff, agentId?: string): void {1348// Hide the widget after selection1349this.chatSuggestNextWidget.hide();13501351const promptToUse = handoff.prompt;13521353// Log telemetry1354const currentMode = this.input.currentModeObs.get();1355const fromAgent = currentMode?.id ?? '';1356this.telemetryService.publicLog2<ChatHandoffClickEvent, ChatHandoffClickClassification>('chat.handoffClicked', {1357fromAgent: fromAgent,1358toAgent: agentId || handoff.agent || '',1359hasPrompt: Boolean(promptToUse),1360autoSend: Boolean(handoff.send)1361});13621363// If agentId is provided (from chevron dropdown), delegate to that chat session1364// Otherwise, switch to the handoff agent1365if (agentId) {1366// Delegate to chat session (e.g., @background or @cloud)1367this.input.setValue(`@${agentId} ${promptToUse}`, false);1368this.input.focus();1369// Auto-submit for delegated chat sessions1370this.acceptInput().catch(e => this.logService.error('Failed to handle handoff continueOn', e));1371} else if (handoff.agent) {1372// Regular handoff to specified agent1373this._switchToAgentByName(handoff.agent);1374// Switch to the specified model if provided1375if (handoff.model) {1376this.input.switchModelByQualifiedName([handoff.model]);1377}1378// Insert the handoff prompt into the input1379this.input.setValue(promptToUse, false);1380this.input.focus();13811382// Auto-submit if send flag is true1383if (handoff.send) {1384this.acceptInput();1385}1386}1387}13881389async handleDelegationExitIfNeeded(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): Promise<void> {1390if (!this._shouldExitAfterDelegation(sourceAgent, targetAgent)) {1391return;1392}13931394this.logService.debug(`[Delegation] Will exit after delegation: sourceAgent=${sourceAgent?.id}, targetAgent=${targetAgent?.id}`);1395try {1396await this._handleDelegationExit();1397} catch (e) {1398this.logService.error('[Delegation] Failed to handle delegation exit', e);1399}1400}14011402private _shouldExitAfterDelegation(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): boolean {1403if (!targetAgent) {1404this.logService.debug('[Delegation] _shouldExitAfterDelegation: false (no targetAgent)');1405return false;1406}14071408if (!this.configurationService.getValue<boolean>(ChatConfiguration.ExitAfterDelegation)) {1409this.logService.debug('[Delegation] _shouldExitAfterDelegation: false (ExitAfterDelegation config disabled)');1410return false;1411}14121413// Never exit if the source and target are the same (that means that you're providing a follow up, etc.)1414// NOTE: sourceAgent would be the chatWidget's 'lockedAgent'1415if (sourceAgent && sourceAgent.id === targetAgent.id) {1416this.logService.debug('[Delegation] _shouldExitAfterDelegation: false (source and target agents are the same)');1417return false;1418}14191420if (!isIChatViewViewContext(this.viewContext)) {1421this.logService.debug('[Delegation] _shouldExitAfterDelegation: false (not in chat view context)');1422return false;1423}14241425const contribution = this.chatSessionsService.getChatSessionContribution(targetAgent.id);1426if (!contribution) {1427this.logService.debug(`[Delegation] _shouldExitAfterDelegation: false (no contribution found for targetAgent.id=${targetAgent.id})`);1428return false;1429}14301431if (contribution.canDelegate !== true) {1432this.logService.debug(`[Delegation] _shouldExitAfterDelegation: false (contribution.canDelegate=${contribution.canDelegate}, expected true)`);1433return false;1434}14351436this.logService.debug('[Delegation] _shouldExitAfterDelegation: true');1437return true;1438}14391440/**1441* Handles the exit of the panel chat when a delegation to another session occurs.1442* Waits for the response to complete and any pending confirmations to be resolved,1443* then clears the widget unless the final message is an error.1444*/1445private async _handleDelegationExit(): Promise<void> {1446const viewModel = this.viewModel;1447if (!viewModel) {1448this.logService.debug('[Delegation] _handleDelegationExit: no viewModel, returning');1449return;1450}14511452const parentSessionResource = viewModel.sessionResource;1453this.logService.debug(`[Delegation] _handleDelegationExit: parentSessionResource=${parentSessionResource.toString()}`);14541455// Check if response is complete, not pending confirmation, and has no error1456const checkIfShouldClear = (): boolean => {1457const items = viewModel.getItems();1458const lastItem = items[items.length - 1];1459if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {1460const hasError = Boolean(lastItem.result?.errorDetails);1461return !hasError;1462}1463return false;1464};14651466if (checkIfShouldClear()) {1467this.logService.debug('[Delegation] Response complete, archiving session before clearing');1468// Archive BEFORE clearing to ensure session still exists in agentSessionsService1469await this.archiveLocalParentSession(parentSessionResource);1470await this.clear();1471return;1472}14731474this.logService.debug('[Delegation] Waiting for response to complete...');1475const shouldClear = await new Promise<boolean>(resolve => {1476const disposable = viewModel.onDidChange(() => {1477const result = checkIfShouldClear();1478if (result) {1479cleanup();1480resolve(true);1481}1482});1483const timeout = setTimeout(() => {1484this.logService.debug('[Delegation] Timeout waiting for response to complete');1485cleanup();1486resolve(false);1487}, 30_000); // 30 second timeout1488const cleanup = () => {1489clearTimeout(timeout);1490disposable.dispose();1491};1492});14931494if (shouldClear) {1495this.logService.debug('[Delegation] Response completed, archiving session before clearing');1496await this.archiveLocalParentSession(parentSessionResource);1497await this.clear();1498} else {1499this.logService.debug('[Delegation] Not clearing (timeout or error)');1500}1501}15021503private async archiveLocalParentSession(sessionResource: URI): Promise<void> {1504if (sessionResource.scheme !== Schemas.vscodeLocalChatSession) {1505this.logService.debug(`[Delegation] archiveLocalParentSession: skipping, scheme=${sessionResource.scheme} is not vscodeLocalChatSession`);1506return;1507}15081509this.logService.debug(`[Delegation] archiveLocalParentSession: archiving session ${sessionResource.toString()}`);15101511// Implicitly keep parent session's changes as they've now been delegated to the new agent.1512await this.chatService.getSession(sessionResource)?.editingSession?.accept();15131514const session = this.agentSessionsService.getSession(sessionResource);1515if (session) {1516session.setArchived(true);1517this.logService.debug('[Delegation] archiveLocalParentSession: session archived successfully');1518} else {1519this.logService.warn(`[Delegation] archiveLocalParentSession: session not found in agentSessionsService for ${sessionResource.toString()}`);1520}1521}15221523setVisible(visible: boolean): void {1524const wasVisible = this._visible;1525this._visible = visible;1526this.visibleChangeCount++;1527this.listWidget.setVisible(visible);1528this.input.setVisible(visible);15291530if (visible) {1531if (!wasVisible) {1532this.visibilityTimeoutDisposable.value = disposableTimeout(() => {1533// Progressive rendering paused while hidden, so start it up again.1534// Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here)1535if (this._visible) {1536this.onDidChangeItems(true);1537}1538}, 0);15391540this.visibilityAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {1541this._onDidShow.fire();1542});1543}1544} else if (wasVisible) {1545this._onDidHide.fire();1546}1547}15481549private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void {1550// Create a dom element to hold UI from editor widgets embedded in chat messages1551const overflowWidgetsContainer = document.createElement('div');1552overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor');1553listContainer.append(overflowWidgetsContainer);15541555// Create chat list widget1556this.listWidget = this._register(this.instantiationService.createInstance(1557ChatListWidget,1558listContainer,1559{1560rendererOptions: options,1561renderStyle: this.viewOptions.renderStyle,1562defaultElementHeight: this.viewOptions.defaultElementHeight ?? 200,1563overflowWidgetsDomNode: overflowWidgetsContainer,1564styles: {1565listForeground: this.styles.listForeground,1566listBackground: this.styles.listBackground,1567},1568currentChatMode: () => this.input.currentModeKind,1569filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions) } : undefined,1570codeBlockModelCollection: this._codeBlockModelCollection,1571viewModel: this.viewModel,1572editorOptions: this.editorOptions,1573location: this.location,1574getCurrentLanguageModelId: () => this.input.currentLanguageModel,1575getCurrentModeInfo: () => this.input.currentModeInfo,1576}1577));15781579// Wire up ChatWidget-specific list widget events1580this._register(this.listWidget.onDidClickRequest(async item => {1581this.clickedRequest(item);1582}));15831584this._register(this.listWidget.onDidRerender(item => {1585if (isRequestVM(item.currentElement) && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1586if (!item.rowContainer.contains(this.inputContainer)) {1587item.rowContainer.appendChild(this.inputContainer);1588}1589this.input.focus();1590}1591}));15921593this._register(this.listWidget.onDidDispose(() => {1594this.focusedInputDOM.appendChild(this.inputContainer);1595this.input.focus();1596}));15971598this._register(this.listWidget.onDidFocusOutside(() => {1599this.finishedEditing();1600}));16011602this._register(this.listWidget.onDidClickFollowup(item => {1603// is this used anymore?1604this.acceptInput(item.message);1605}));16061607this._register(this.listWidget.onDidChangeContentHeight(() => {1608this._onDidChangeContentHeight.fire();1609}));16101611this._register(this.listWidget.onDidFocus(() => {1612this._onDidFocus.fire();1613}));1614this._register(this.listWidget.onDidScroll(() => {1615this._onDidScroll.fire();1616}));1617}16181619startEditing(requestId: string): void {1620const editedRequest = this.listWidget.getTemplateDataForRequestId(requestId);1621if (editedRequest) {1622this.clickedRequest(editedRequest);1623}1624}16251626private clickedRequest(item: IChatListItemTemplate) {16271628const currentElement = item.currentElement;1629if (isRequestVM(currentElement) && !this.viewModel?.editing) {16301631const requests = this.viewModel?.model.getRequests();1632if (!requests || !this.viewModel?.sessionResource) {1633return;1634}16351636// this will only ever be true if we restored a checkpoint1637if (this.viewModel?.model.checkpoint) {1638this.recentlyRestoredCheckpoint = true;1639}16401641this.viewModel?.model.setCheckpoint(currentElement.id);16421643// set contexts and request to false1644const currentContext: IChatRequestVariableEntry[] = [];1645const addedContextIds = new Set<string>();1646const addToContext = (entry: IChatRequestVariableEntry) => {1647if (addedContextIds.has(entry.id) || isWorkspaceVariableEntry(entry)) {1648return;1649}1650if ((isPromptFileVariableEntry(entry) || isPromptTextVariableEntry(entry)) && entry.automaticallyAdded) {1651return;1652}1653addedContextIds.add(entry.id);1654currentContext.push(entry);1655};1656for (let i = requests.length - 1; i >= 0; i -= 1) {1657const request = requests[i];1658if (request.id === currentElement.id) {1659request.setShouldBeBlocked(false); // unblocking just this request.1660request.attachedContext?.forEach(addToContext);1661}1662}1663currentElement.variables.forEach(addToContext);16641665// set states1666this.viewModel?.setEditing(currentElement);1667if (item?.contextKeyService) {1668ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true);1669}16701671const isEditingSentRequest = currentElement.pendingKind === undefined ? ChatContextKeys.EditingRequestType.Sent : ChatContextKeys.EditingRequestType.QueueOrSteer;1672const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';1673this.inputPart?.setEditing(!!this.viewModel?.editing && isInput, isEditingSentRequest);16741675if (!isInput) {1676const rowContainer = item.rowContainer;1677this.inputContainer = dom.$('.chat-edit-input-container');1678rowContainer.appendChild(this.inputContainer);1679this.createInput(this.inputContainer);1680this.input.setChatMode(this.inputPart.currentModeObs.get().id);1681this.input.setEditing(true, isEditingSentRequest);1682} else {1683this.inputPart.element.classList.add('editing');1684}16851686this.inputPart.toggleChatInputOverlay(!isInput);1687if (currentContext.length > 0) {1688this.input.attachmentModel.addContext(...currentContext);1689}16901691// rerenders1692this.inputPart.dnd.setDisabledOverlay(!isInput);1693this.input.renderAttachedContext();1694this.input.setValue(currentElement.messageText, false);1695this.listWidget.suppressAutoScroll = true;1696this.onDidChangeItems();1697this.input.inputEditor.focus();16981699this._register(this.inputPart.onDidClickOverlay(() => {1700if (this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1701this.finishedEditing();1702}1703}));17041705// listeners1706if (!isInput) {1707this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => {1708this.listWidget.scrollToCurrentItem(currentElement);1709}));17101711this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => {1712this.listWidget.scrollToCurrentItem(currentElement);1713}));1714}1715}17161717type StartRequestEvent = { editRequestType: string };17181719type StartRequestEventClassification = {1720owner: 'justschen';1721comment: 'Event used to gain insights into when edits are being pressed.';1722editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1723};17241725this.telemetryService.publicLog2<StartRequestEvent, StartRequestEventClassification>('chat.startEditingRequests', {1726editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1727});1728}17291730finishedEditing(completedEdit?: boolean): void {1731// reset states1732this.listWidget.suppressAutoScroll = false;1733const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id);1734if (this.recentlyRestoredCheckpoint) {1735this.recentlyRestoredCheckpoint = false;1736} else {1737this.viewModel?.model.setCheckpoint(undefined);1738}1739this.inputPart.dnd.setDisabledOverlay(false);1740if (editedRequest?.contextKeyService) {1741ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false);1742}17431744const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';17451746if (!isInput) {1747this.inputPart.setChatMode(this.input.currentModeObs.get().id);1748const currentModel = this.input.selectedLanguageModel.get();1749if (currentModel) {1750this.inputPart.switchModel(currentModel.metadata);1751}17521753this.inputPart?.toggleChatInputOverlay(false);1754try {1755if (editedRequest?.rowContainer?.contains(this.inputContainer)) {1756editedRequest.rowContainer.removeChild(this.inputContainer);1757} else if (this.inputContainer.parentElement) {1758this.inputContainer.parentElement.removeChild(this.inputContainer);1759}1760} catch (e) {1761this.logService.error('Error occurred while finishing editing:', e);1762}1763this.inputContainer = dom.$('.empty-chat-state');17641765// only dispose if we know the input is not the bottom input object.1766this.input.dispose();1767}17681769if (isInput) {1770this.inputPart.element.classList.remove('editing');1771}1772this.viewModel?.setEditing(undefined);1773this.inputPart?.setEditing(false, undefined);17741775this.onDidChangeItems();17761777type CancelRequestEditEvent = {1778editRequestType: string;1779editCanceled: boolean;1780};17811782type CancelRequestEventEditClassification = {1783owner: 'justschen';1784editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1785editCanceled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether the edit was canceled.' };1786comment: 'Event used to gain insights into when edits are being canceled.';1787};17881789this.telemetryService.publicLog2<CancelRequestEditEvent, CancelRequestEventEditClassification>('chat.editRequestsFinished', {1790editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1791editCanceled: !completedEdit1792});17931794this.inputPart.focus();1795}17961797private getWidgetViewKindTag(): string {1798if (!this.viewContext) {1799return 'editor';1800} else if (isIChatViewViewContext(this.viewContext)) {1801return 'view';1802} else {1803return 'quick';1804}1805}18061807private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'compact' | 'minimal'; renderInputToolbarBelowInput?: boolean }): void {1808const commonConfig: IChatInputPartOptions = {1809renderFollowups: options?.renderFollowups ?? true,1810renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle,1811renderInputToolbarBelowInput: options?.renderInputToolbarBelowInput ?? false,1812menus: {1813executeToolbar: MenuId.ChatExecute,1814telemetrySource: 'chatWidget',1815...this.viewOptions.menus1816},1817editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,1818enableImplicitContext: this.viewOptions.enableImplicitContext,1819renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit',1820supportsChangingModes: this.viewOptions.supportsChangingModes,1821dndContainer: this.viewOptions.dndContainer,1822widgetViewKindTag: this.getWidgetViewKindTag(),1823defaultMode: this.viewOptions.defaultMode,1824sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate,1825workspacePickerDelegate: this.viewOptions.workspacePickerDelegate,1826};18271828if (this.viewModel?.editing) {1829const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id);1830const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService])));1831this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart,1832this.location,1833commonConfig,1834this.styles,1835true1836);1837} else {1838this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart,1839this.location,1840commonConfig,1841this.styles,1842false1843);1844this._register(autorun(reader => {1845this.inputPart.height.read(reader);1846if (!this.listWidget) {1847// This is set up before the list/renderer are created1848return;1849}18501851if (this.bodyDimension) {1852this.layout(this.bodyDimension.height, this.bodyDimension.width);1853}18541855// Keep getting-started tip aligned with the top of the input1856this.layoutGettingStartedTipPosition();18571858this._onDidChangeContentHeight.fire();1859}));1860}18611862this.input.render(container, '', this);1863if (this.bodyDimension?.width) {1864this.input.layout(this.bodyDimension.width);1865}18661867this._register(this.input.onDidLoadInputState(() => {1868this.refreshParsedInput();1869}));1870this._register(this.input.onDidFocus(() => this._onDidFocus.fire()));1871this._register(this.input.onDidAcceptFollowup(e => {1872if (!this.viewModel) {1873return;1874}18751876let msg = '';1877if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind)?.id) {1878const agent = this.chatAgentService.getAgent(e.followup.agentId);1879if (!agent) {1880return;1881}18821883this.lastSelectedAgent = agent;1884msg = `${chatAgentLeader}${agent.name} `;1885if (e.followup.subCommand) {1886msg += `${chatSubcommandLeader}${e.followup.subCommand} `;1887}1888} else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) {1889msg = `${chatSubcommandLeader}${e.followup.subCommand} `;1890}18911892msg += e.followup.message;1893this.acceptInput(msg);18941895if (!e.response) {1896// Followups can be shown by the welcome message, then there is no response associated.1897// At some point we probably want telemetry for these too.1898return;1899}19001901this.chatService.notifyUserAction({1902sessionResource: this.viewModel.sessionResource,1903requestId: e.response.requestId,1904agentId: e.response.agent?.id,1905command: e.response.slashCommand?.name,1906result: e.response.result,1907action: {1908kind: 'followUp',1909followup: e.followup1910},1911});1912}));1913this._register(this.inputEditor.onDidChangeModelContent(() => {1914this.parsedChatRequest = undefined;1915this.updateChatInputContext();1916}));1917this._register(this.chatAgentService.onDidChangeAgents(() => {1918this.parsedChatRequest = undefined;1919// Tools agent loads -> welcome content changes1920this.renderWelcomeViewContentIfNeeded();1921}));1922this._register(this.input.onDidChangeCurrentChatMode(() => {1923this.renderWelcomeViewContentIfNeeded();1924this.refreshParsedInput();1925this.renderFollowups();1926this.renderChatSuggestNextWidget();1927}));19281929this._register(autorun(r => {1930const toolSetIds = new Set<string>();1931const toolIds = new Set<string>();1932for (const [entry, enabled] of this.input.selectedToolsModel.entriesMap.read(r)) {1933if (enabled) {1934if (isToolSet(entry)) {1935toolSetIds.add(entry.id);1936} else {1937toolIds.add(entry.id);1938}1939}1940}1941const disabledTools = this.input.attachmentModel.attachments1942.filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id))1943.map(a => a.id);19441945this.input.attachmentModel.updateContext(disabledTools, Iterable.empty());1946this.refreshParsedInput();1947}));1948}19491950private onDidStyleChange(): void {1951this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? '');1952this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? '');1953this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? '');1954}195519561957setModel(model: IChatModel | undefined): void {1958if (!this.container) {1959throw new Error('Call render() before setModel()');1960}19611962if (!model) {1963if (this.viewModel?.editing) {1964this.finishedEditing();1965}1966this.viewModel = undefined;1967this.onDidChangeItems();1968this._hasPendingRequestsContextKey.set(false);1969return;1970}19711972if (isEqual(model.sessionResource, this.viewModel?.sessionResource)) {1973return;1974}19751976if (this.viewModel?.editing) {1977this.finishedEditing();1978}1979this.inputPart.clearTodoListWidget(model.sessionResource, false);1980this.chatSuggestNextWidget.hide();1981this.chatTipService.resetSession();19821983this._codeBlockModelCollection.clear();19841985this.container.setAttribute('data-session-id', model.sessionId);1986this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined);19871988// Pass input model reference to input part for state syncing1989this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0);1990this.listWidget.setViewModel(this.viewModel);19911992if (this._lockedAgent) {1993let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id);1994if (!placeholder) {1995placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgent.id);1996}1997this.viewModel.setInputPlaceholder(placeholder);1998this.inputEditor.updateOptions({ placeholder });1999} else if (this.viewModel.inputPlaceholder) {2000this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });2001}20022003const renderImmediately = this.configurationService.getValue<boolean>('chat.experimental.renderMarkdownImmediately');2004const delay = renderImmediately ? MicrotaskDelay : 0;2005this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, delay), (events => {2006if (!this.viewModel || this._store.isDisposed) {2007// See https://github.com/microsoft/vscode/issues/2789692008return;2009}20102011this.requestInProgress.set(this.viewModel.model.requestInProgress.get());20122013// Update the editor's placeholder text when it changes in the view model2014if (events?.some(e => e?.kind === 'changePlaceholder')) {2015this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });2016}20172018this.onDidChangeItems();2019if (events?.some(e => e?.kind === 'addRequest') && this.visible) {2020this.listWidget.scrollToEnd();2021}2022})));2023this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => {2024// Ensure that view state is saved here, because we will load it again when a new model is assigned2025if (this.viewModel?.editing) {2026this.finishedEditing();2027}2028// Disposes the viewmodel and listeners2029this.viewModel = undefined;2030this.onDidChangeItems();2031}));2032this._sessionIsEmptyContextKey.set(model.getRequests().length === 0);2033const updatePendingRequestKeys = () => {2034const pendingCount = model.getPendingRequests().length;2035this._hasPendingRequestsContextKey.set(pendingCount > 0);2036};2037updatePendingRequestKeys();2038this.viewModelDisposables.add(model.onDidChangePendingRequests(() => updatePendingRequestKeys()));20392040this.refreshParsedInput();2041this.viewModelDisposables.add(model.onDidChange((e) => {2042if (e.kind === 'setAgent') {2043this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command });2044// Update capabilities context keys when agent changes2045this._updateAgentCapabilitiesContextKeys(e.agent);2046}2047if (e.kind === 'addRequest') {2048this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false);2049this._sessionIsEmptyContextKey.set(false);2050}2051// Hide widget on request removal2052if (e.kind === 'removeRequest') {2053this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);2054this.chatSuggestNextWidget.hide();2055this._sessionIsEmptyContextKey.set((this.viewModel?.model.getRequests().length ?? 0) === 0);2056}2057// Show next steps widget when response completes (not when request starts)2058if (e.kind === 'completedRequest') {2059const lastRequest = this.viewModel?.model.getRequests().at(-1);2060const wasCancelled = lastRequest?.response?.isCanceled ?? false;2061if (wasCancelled) {2062// Clear todo list when request is cancelled2063this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);2064}2065// Only show if response wasn't canceled2066this.renderChatSuggestNextWidget();20672068// Mark the session as read when the request completes and the widget is visible2069if (this.visible && this.viewModel?.sessionResource) {2070this.agentSessionsService.getSession(this.viewModel.sessionResource)?.setRead(true);2071}2072}2073}));20742075if (this.listWidget && this.visible) {2076this.onDidChangeItems();2077this.listWidget.scrollToEnd();2078}20792080this.updateChatInputContext();2081this.input.renderChatTodoListWidget(this.viewModel.sessionResource);2082}20832084getFocus(): ChatTreeItem | undefined {2085return this.listWidget.getFocus()[0] ?? undefined;2086}20872088reveal(item: ChatTreeItem, relativeTop?: number): void {2089this.listWidget.reveal(item, relativeTop);2090}20912092focus(item: ChatTreeItem): void {2093if (!this.listWidget.hasElement(item)) {2094return;2095}20962097this.listWidget.focusItem(item);2098}20992100setInputPlaceholder(placeholder: string): void {2101this.viewModel?.setInputPlaceholder(placeholder);2102}21032104resetInputPlaceholder(): void {2105this.viewModel?.resetInputPlaceholder();2106}21072108setInput(value = ''): void {2109this.input.setValue(value, false);2110this.refreshParsedInput();2111}21122113getInput(): string {2114return this.input.inputEditor.getValue();2115}21162117getContrib<T extends IChatWidgetContrib>(id: string): T | undefined {2118return this.contribs.find(c => c.id === id) as T | undefined;2119}21202121// Coding agent locking methods2122lockToCodingAgent(name: string, displayName: string, agentId: string): void {2123this._lockedAgent = {2124id: agentId,2125name,2126prefix: `@${name} `,2127displayName2128};2129this._lockedToCodingAgentContextKey.set(true);2130this.renderWelcomeViewContentIfNeeded();2131// Update capabilities for the locked agent2132const agent = this.chatAgentService.getAgent(agentId);2133this._updateAgentCapabilitiesContextKeys(agent);2134this.listWidget?.updateRendererOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true });2135if (this.visible) {2136this.listWidget?.rerender();2137}2138}21392140unlockFromCodingAgent(): void {2141// Clear all state related to locking2142this._lockedAgent = undefined;2143this._lockedToCodingAgentContextKey.set(false);2144this._updateAgentCapabilitiesContextKeys(undefined);21452146// Explicitly update the DOM to reflect unlocked state2147this.renderWelcomeViewContentIfNeeded();21482149// Reset to default placeholder2150if (this.viewModel) {2151this.viewModel.resetInputPlaceholder();2152}2153this.inputEditor?.updateOptions({ placeholder: undefined });2154this.listWidget?.updateRendererOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask });2155if (this.visible) {2156this.listWidget?.rerender();2157}2158}21592160get isLockedToCodingAgent(): boolean {2161return !!this._lockedAgent;2162}21632164get lockedAgentId(): string | undefined {2165return this._lockedAgent?.id;2166}21672168logInputHistory(): void {2169this.input.logInputHistory();2170}21712172async acceptInput(query?: string, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {2173return this._acceptInput(query ? { query } : undefined, options);2174}21752176async rerunLastRequest(): Promise<void> {2177if (!this.viewModel) {2178return;2179}21802181const sessionResource = this.viewModel.sessionResource;2182const lastRequest = this.chatService.getSession(sessionResource)?.getRequests().at(-1);2183if (!lastRequest) {2184return;2185}21862187const options: IChatSendRequestOptions = {2188attempt: lastRequest.attempt + 1,2189location: this.location,2190userSelectedModelId: this.input.currentLanguageModel2191};2192return await this.chatService.resendRequest(lastRequest, options);2193}21942195private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise<void> {2196// first check if the input has a prompt slash command2197const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart);2198if (!agentSlashPromptPart) {2199return;2200}22012202// need to resolve the slash command to get the prompt file2203const slashCommand = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.name, CancellationToken.None);2204if (!slashCommand) {2205return;2206}2207const parseResult = slashCommand.parsedPromptFile;2208// add the prompt file to the context2209const refs = parseResult.body?.variableReferences.map(({ name, offset }) => ({ name, range: new OffsetRange(offset, offset + name.length + 1) })) ?? [];2210const toolReferences = this.toolsService.toToolReferences(refs);2211requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));22122213// remove the slash command from the input2214requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim();22152216const input = requestInput.input.trim();2217requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`;2218if (input) {2219// if the input is not empty, append it to the prompt2220requestInput.input += `\n${input}`;2221}2222if (parseResult.header) {2223await this._applyPromptMetadata(parseResult.header, requestInput);2224}2225}22262227private async _acceptInput(query: { query: string } | undefined, options: IChatAcceptInputOptions = {}): Promise<IChatResponseModel | undefined> {2228if (!query && this.input.generating) {2229// if the user submits the input and generation finishes quickly, just submit it for them2230const generatingAutoSubmitWindow = 500;2231const start = Date.now();2232await this.input.generating;2233if (Date.now() - start > generatingAutoSubmitWindow) {2234return;2235}2236}22372238while (!this._viewModel && !this._store.isDisposed) {2239await Event.toPromise(this.onDidChangeViewModel, this._store);2240}22412242if (!this.viewModel) {2243return;2244}22452246// Check if a custom submit handler wants to handle this submission2247if (this.viewOptions.submitHandler) {2248const inputValue = !query ? this.getInput() : query.query;2249const handled = await this.viewOptions.submitHandler(inputValue, this.input.currentModeKind);2250if (handled) {2251return;2252}2253}22542255this._onDidAcceptInput.fire();2256this.listWidget.setScrollLock(this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll));22572258const editorValue = this.getInput();2259const requestInputs: IChatRequestInputOptions = {2260input: !query ? editorValue : query.query,2261attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionResource) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionResource),2262};22632264const isUserQuery = !query;2265const isEditing = this.viewModel?.editing;2266if (isEditing) {2267const editingPendingRequest = this.viewModel.editing!.pendingKind;2268if (editingPendingRequest !== undefined) {2269const editingRequestId = this.viewModel.editing!.id;2270this.chatService.removePendingRequest(this.viewModel.sessionResource, editingRequestId);2271options.queue ??= editingPendingRequest;2272} else {2273this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource);2274options.queue = undefined;2275}22762277this.finishedEditing(true);2278this.viewModel.model?.setCheckpoint(undefined);2279}22802281const model = this.viewModel.model;2282const requestInProgress = model.requestInProgress.get();2283if (requestInProgress) {2284options.queue ??= ChatRequestQueueKind.Queued;2285}2286if (!requestInProgress && !isEditing && !(await this.confirmPendingRequestsBeforeSend(model, options))) {2287return;2288}22892290// process the prompt command2291await this._applyPromptFileIfSet(requestInputs);2292await this._autoAttachInstructions(requestInputs);22932294if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) {2295const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set2296const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext;22972298// Collect file variables from previous requests before sending the request2299const previousRequests = this.viewModel.model.getRequests();2300for (const request of previousRequests) {2301for (const variable of request.variableData.variables) {2302if (URI.isUri(variable.value) && variable.kind === 'file') {2303const uri = variable.value;2304if (!uniqueWorkingSetEntries.has(uri)) {2305editingSessionAttachedContext.add(variable);2306uniqueWorkingSetEntries.add(variable.value);2307}2308}2309}2310}2311requestInputs.attachedContext = editingSessionAttachedContext;23122313type ChatEditingWorkingSetClassification = {2314owner: 'joyceerhl';2315comment: 'Information about the working set size in a chat editing request';2316originalSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that the user tried to attach in their editing request.' };2317actualSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that were actually sent in their editing request.' };2318};2319type ChatEditingWorkingSetEvent = {2320originalSize: number;2321actualSize: number;2322};2323this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size });2324}23252326this.input.validateAgentMode();23272328if (this.viewModel.model.checkpoint) {2329const requests = this.viewModel.model.getRequests();2330for (let i = requests.length - 1; i >= 0; i -= 1) {2331const request = requests[i];2332if (request.shouldBeBlocked) {2333this.chatService.removeRequest(this.viewModel.sessionResource, request.id);2334}2335}2336}2337// Expand directory attachments: extract images as binary entries2338const resolvedImageVariables = await this._resolveDirectoryImageAttachments(requestInputs.attachedContext.asArray());23392340if (this.viewModel.sessionResource && !options.queue) {2341// todo@connor4312: move chatAccessibilityService.acceptRequest to a refcount model to handle queue messages2342this.chatAccessibilityService.acceptRequest(this._viewModel!.sessionResource);2343}23442345const result = await this.chatService.sendRequest(this.viewModel.sessionResource, requestInputs.input, {2346userSelectedModelId: this.input.currentLanguageModel,2347location: this.location,2348locationData: this._location.resolveData?.(),2349parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind },2350attachedContext: requestInputs.attachedContext.asArray(),2351resolvedVariables: resolvedImageVariables,2352noCommandDetection: options?.noCommandDetection,2353...this.getModeRequestOptions(),2354modeInfo: this.input.currentModeInfo,2355agentIdSilent: this._lockedAgent?.id,2356queue: options?.queue,2357});23582359if (this.viewModel.sessionResource && !options.queue && ChatSendResult.isRejected(result)) {2360this.chatAccessibilityService.disposeRequest(this.viewModel.sessionResource);2361}23622363if (ChatSendResult.isRejected(result)) {2364return;2365}23662367// visibility sync before firing events to hide the welcome view2368this.updateChatViewVisibility();2369this.input.acceptInput(options?.storeToHistory ?? isUserQuery);23702371const sent = ChatSendResult.isQueued(result) ? await result.deferred : result;2372if (!ChatSendResult.isSent(sent)) {2373return;2374}23752376// If this was a queued request that just got dequeued, start the progress sound now2377if (options.queue && this.viewModel?.sessionResource) {2378this.chatAccessibilityService.acceptRequest(this.viewModel.sessionResource);2379}23802381this._onDidSubmitAgent.fire({ agent: sent.data.agent, slashCommand: sent.data.slashCommand });2382this.handleDelegationExitIfNeeded(this._lockedAgent, sent.data.agent);2383sent.data.responseCompletePromise.then(() => {2384const responses = this.viewModel?.getItems().filter(isResponseVM);2385const lastResponse = responses?.[responses.length - 1];2386this.chatAccessibilityService.acceptResponse(this, this.container, lastResponse, this.viewModel?.sessionResource, options?.isVoiceInput);2387if (lastResponse?.result?.nextQuestion) {2388const { prompt, participant, command } = lastResponse.result.nextQuestion;2389const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command);2390if (question) {2391this.input.setValue(question, false);2392}2393}2394});23952396return sent.data.responseCreatedPromise;2397}23982399// Resolve images from directory attachments to send as additional variables.2400private async _resolveDirectoryImageAttachments(attachments: IChatRequestVariableEntry[]): Promise<IChatRequestVariableEntry[]> {2401const imagePromises: Promise<IChatRequestVariableEntry[]>[] = [];24022403for (const attachment of attachments) {2404if (attachment.kind === 'directory' && URI.isUri(attachment.value)) {2405imagePromises.push(2406this.chatAttachmentResolveService.resolveDirectoryImages(attachment.value)2407);2408}2409}24102411if (imagePromises.length === 0) {2412return [];2413}24142415const resolved = await Promise.all(imagePromises);2416return resolved.flat();2417}24182419private async confirmPendingRequestsBeforeSend(model: IChatModel, options: IChatAcceptInputOptions): Promise<boolean> {2420if (options.queue) {2421return true;2422}24232424const hasPendingRequests = model.getPendingRequests().length > 0;2425if (!hasPendingRequests) {2426return true;2427}24282429const promptResult = await this.dialogService.prompt({2430type: 'question',2431message: localize('chat.pendingRequests.prompt.message', "You already have pending requests."),2432detail: localize('chat.pendingRequests.prompt.detail', "Do you want to keep them in the queue or remove them before sending this message?"),2433buttons: [2434{2435label: localize('chat.pendingRequests.prompt.keep', "Keep Pending Requests"),2436run: () => 'keep'2437},2438{2439label: localize('chat.pendingRequests.prompt.remove', "Remove Pending Requests"),2440run: () => 'remove'2441}2442],2443cancelButton: true2444});24452446if (!promptResult.result) {2447return false;2448}24492450if (promptResult.result === 'remove') {2451for (const pendingRequest of [...model.getPendingRequests()]) {2452this.chatService.removePendingRequest(model.sessionResource, pendingRequest.request.id);2453}2454}24552456return true;2457}24582459getModeRequestOptions(): Partial<IChatSendRequestOptions> {2460return {2461modeInfo: this.input.currentModeInfo,2462userSelectedTools: this.input.selectedToolsModel.userSelectedTools,2463};2464}24652466getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] {2467return this.listWidget.getCodeBlockInfosForResponse(response);2468}24692470getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined {2471return this.listWidget.getCodeBlockInfoForEditor(uri);2472}24732474getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] {2475return this.listWidget.getFileTreeInfosForResponse(response);2476}24772478getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined {2479return this.listWidget.getLastFocusedFileTreeForResponse(response);2480}24812482focusResponseItem(lastFocused?: boolean): void {2483this.listWidget.focusLastItem(lastFocused);2484}24852486layout(height: number, width: number): void {2487width = Math.min(width, this.viewOptions.renderStyle === 'minimal' ? width : 950); // no min width of inline chat24882489this.bodyDimension = new dom.Dimension(width, height);24902491if (this.viewModel?.editing) {2492this.inlineInputPart?.layout(width);2493}24942495this.inputPart.layout(width);24962497const inputHeight = this.inputPart.height.get();2498const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;2499const lastElementVisible = this.listWidget.isScrolledToBottom;2500const lastItem = this.listWidget.lastItem;25012502const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight);2503this.listWidget.layout(contentHeight, width);25042505this.welcomeMessageContainer.style.height = `${contentHeight}px`;25062507const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;2508if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) {2509this.listWidget.scrollToEnd();2510}2511this.listContainer.style.height = `${contentHeight}px`;25122513this._onDidChangeHeight.fire(height);2514}25152516private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean };25172518// An alternative to layout, this allows you to specify the number of ChatTreeItems2519// you want to show, and the max height of the container. It will then layout the2520// tree to show that many items.2521// TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used2522setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2523this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2524this._register(this.listWidget.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode()));25252526const mutableDisposable = this._register(new MutableDisposable());2527this._register(this.listWidget.onDidScroll((e) => {2528// TODO@TylerLeonhardt this should probably just be disposed when this is disabled2529// and then set up again when it is enabled again2530if (!this._dynamicMessageLayoutData?.enabled) {2531return;2532}2533mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {2534if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) {2535return;2536}2537const renderHeight = e.height;2538const diff = e.scrollHeight - renderHeight - e.scrollTop;2539if (diff === 0) {2540return;2541}25422543const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight);2544const width = this.bodyDimension?.width ?? this.container.offsetWidth;2545this.input.layout(width);2546const inputPartHeight = this.input.height.get();2547const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;2548const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight);2549this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width);2550});2551}));2552}25532554updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2555this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2556let hasChanged = false;2557let height = this.bodyDimension!.height;2558let width = this.bodyDimension!.width;2559if (maxHeight < this.bodyDimension!.height) {2560height = maxHeight;2561hasChanged = true;2562}2563const containerWidth = this.container.offsetWidth;2564if (this.bodyDimension?.width !== containerWidth) {2565width = containerWidth;2566hasChanged = true;2567}2568if (hasChanged) {2569this.layout(height, width);2570}2571}25722573get isDynamicChatTreeItemLayoutEnabled(): boolean {2574return this._dynamicMessageLayoutData?.enabled ?? false;2575}25762577set isDynamicChatTreeItemLayoutEnabled(value: boolean) {2578if (!this._dynamicMessageLayoutData) {2579return;2580}2581this._dynamicMessageLayoutData.enabled = value;2582}25832584layoutDynamicChatTreeItemMode(): void {2585if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) {2586return;2587}25882589const width = this.bodyDimension?.width ?? this.container.offsetWidth;2590this.input.layout(width);2591const inputHeight = this.input.height.get();2592const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;25932594const totalMessages = this.viewModel.getItems();2595// grab the last N messages2596const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages);25972598const needsRerender = messages.some(m => m.currentRenderedHeight === undefined);2599const listHeight = needsRerender2600? this._dynamicMessageLayoutData.maxHeight2601: messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0);26022603this.layout(2604Math.min(2605// we add an additional 18px in order to show that there is scrollable content2606inputHeight + chatSuggestNextWidgetHeight + listHeight + (totalMessages.length > 2 ? 18 : 0),2607this._dynamicMessageLayoutData.maxHeight2608),2609width2610);26112612if (needsRerender || !listHeight) {2613this.listWidget.scrollToEnd();2614}2615}26162617saveState(): void {2618// no-op2619}26202621getViewState(): IChatModelInputState | undefined {2622return this.input.getCurrentInputState();2623}26242625private updateChatInputContext() {2626const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart);2627this.agentInInput.set(!!currentAgent);2628}26292630private async _switchToAgentByName(agentName: string): Promise<void> {2631const currentAgent = this.input.currentModeObs.get();26322633// switch to appropriate agent if needed2634if (agentName !== currentAgent.name.get()) {2635// Find the mode object to get its kind2636const agent = this.chatModeService.findModeByName(agentName);2637if (agent) {2638if (currentAgent.kind !== agent.kind) {2639const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentAgent.kind, agent.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model);2640if (!chatModeCheck) {2641return;2642}26432644if (chatModeCheck.needToClearSession) {2645await this.clear();2646}2647}2648this.input.setChatMode(agent.id);2649}2650}2651}26522653private async _applyPromptMetadata({ agent, tools, model }: PromptHeader, requestInput: IChatRequestInputOptions): Promise<void> {26542655if (tools !== undefined && !agent && this.input.currentModeKind !== ChatModeKind.Agent) {2656agent = ChatMode.Agent.name.get();2657}2658// switch to appropriate agent if needed2659if (agent) {2660this._switchToAgentByName(agent);2661}26622663// if not tools to enable are present, we are done2664if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) {2665const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools, this.input.selectedLanguageModel.get()?.metadata);2666this.input.selectedToolsModel.set(enablementMap, true);2667}26682669if (model !== undefined) {2670this.input.switchModelByQualifiedName(model);2671}2672}26732674/**2675* Adds additional instructions to the context2676* - instructions that have a 'applyTo' pattern that matches the current input2677* - instructions referenced in the copilot settings 'copilot-instructions'2678* - instructions referenced in an already included instruction file2679*/2680private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {2681this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`);2682const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.userSelectedTools.get() : undefined;2683const enabledSubAgents = this.input.currentModeKind === ChatModeKind.Agent ? this.input.currentModeObs.get().agents?.get() : undefined;2684const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeKind, enabledTools, enabledSubAgents);2685await computer.collect(attachedContext, CancellationToken.None);2686}26872688delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {2689this.listWidget.delegateScrollFromMouseWheelEvent(browserEvent);2690}2691}269226932694