Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
4780 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 { Button } from '../../../../../base/browser/ui/button/button.js';11import { ITreeContextMenuEvent, ITreeElement } from '../../../../../base/browser/ui/tree/tree.js';12import { disposableTimeout, timeout } from '../../../../../base/common/async.js';13import { CancellationToken } from '../../../../../base/common/cancellation.js';14import { Codicon } from '../../../../../base/common/codicons.js';15import { toErrorMessage } from '../../../../../base/common/errorMessage.js';16import { Emitter, Event } from '../../../../../base/common/event.js';17import { FuzzyScore } from '../../../../../base/common/filters.js';18import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';19import { Iterable } from '../../../../../base/common/iterator.js';20import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js';21import { ResourceSet } from '../../../../../base/common/map.js';22import { Schemas } from '../../../../../base/common/network.js';23import { filter } from '../../../../../base/common/objects.js';24import { autorun, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';25import { basename, extUri, isEqual } from '../../../../../base/common/resources.js';26import { MicrotaskDelay } from '../../../../../base/common/symbols.js';27import { isDefined } from '../../../../../base/common/types.js';28import { URI } from '../../../../../base/common/uri.js';29import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';30import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';31import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';32import { localize } from '../../../../../nls.js';33import { MenuId } from '../../../../../platform/actions/common/actions.js';34import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';35import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';36import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';37import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js';38import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';39import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';40import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js';41import { ILogService } from '../../../../../platform/log/common/log.js';42import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js';43import product from '../../../../../platform/product/common/product.js';44import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';45import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js';46import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js';47import { IThemeService } from '../../../../../platform/theme/common/themeService.js';48import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';49import { EditorResourceAccessor } from '../../../../common/editor.js';50import { IEditorService } from '../../../../services/editor/common/editorService.js';51import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';52import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';53import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js';54import { checkModeOption } from '../../common/chat.js';55import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';56import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';57import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';58import { IChatLayoutService } from '../../common/widget/chatLayoutService.js';59import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../common/model/chatModel.js';60import { ChatMode, IChatModeService } from '../../common/chatModes.js';61import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js';62import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';63import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js';64import { IChatSessionsService } from '../../common/chatSessionsService.js';65import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js';66import { IChatTodoListService } from '../../common/tools/chatTodoListService.js';67import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';68import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';69import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js';70import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';71import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js';72import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js';73import { PromptsConfig } from '../../common/promptSyntax/config/config.js';74import { IHandOff, PromptHeader, Target } from '../../common/promptSyntax/promptFileParser.js';75import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';76import { handleModeSwitch } from '../actions/chatActions.js';77import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js';78import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js';79import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js';80import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js';81import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/chatInputPart.js';82import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js';83import { ChatEditorOptions } from './chatOptions.js';84import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js';85import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';8687const $ = dom.$;8889export interface IChatWidgetStyles extends IChatInputStyles {90readonly inputEditorBackground: string;91readonly resultEditorBackground: string;92}9394export interface IChatWidgetContrib extends IDisposable {9596readonly id: string;9798/**99* A piece of state which is related to the input editor of the chat widget.100* Takes in the `contrib` object that will be saved in the {@link IChatModelInputState}.101*/102getInputState?(contrib: Record<string, unknown>): void;103104/**105* Called with the result of getInputState when navigating input history.106*/107setInputState?(contrib: Readonly<Record<string, unknown>>): void;108}109110interface IChatRequestInputOptions {111input: string;112attachedContext: ChatRequestVariableSet;113}114115export interface IChatWidgetLocationOptions {116location: ChatAgentLocation;117118resolveData?(): IChatLocationData | undefined;119}120121export function isQuickChat(widget: IChatWidget): boolean {122return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isQuickChat);123}124125function isInlineChat(widget: IChatWidget): boolean {126return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isInlineChat);127}128129type ChatHandoffClickEvent = {130fromAgent: string;131toAgent: string;132hasPrompt: boolean;133autoSend: boolean;134};135136type ChatHandoffClickClassification = {137owner: 'digitarald';138comment: 'Event fired when a user clicks on a handoff prompt in the chat suggest-next widget';139fromAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode the user was in before clicking the handoff' };140toAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode specified in the handoff' };141hasPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff includes a prompt' };142autoSend: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff automatically submits the request' };143};144145type ChatHandoffWidgetShownEvent = {146agent: string;147handoffCount: number;148};149150type ChatHandoffWidgetShownClassification = {151owner: 'digitarald';152comment: 'Event fired when the suggest-next widget is shown with handoff prompts';153agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current agent/mode that has handoffs defined' };154handoffCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of handoff options shown to the user' };155};156157const supportsAllAttachments: Required<IChatAgentAttachmentCapabilities> = {158supportsFileAttachments: true,159supportsToolAttachments: true,160supportsMCPAttachments: true,161supportsImageAttachments: true,162supportsSearchResultAttachments: true,163supportsInstructionAttachments: true,164supportsSourceControlAttachments: true,165supportsProblemAttachments: true,166supportsSymbolAttachments: true,167supportsTerminalAttachments: true,168};169170const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate.");171172export class ChatWidget extends Disposable implements IChatWidget {173174// eslint-disable-next-line @typescript-eslint/no-explicit-any175static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = [];176177private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());178readonly onDidSubmitAgent = this._onDidSubmitAgent.event;179180private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());181readonly onDidChangeAgent = this._onDidChangeAgent.event;182183private _onDidFocus = this._register(new Emitter<void>());184readonly onDidFocus = this._onDidFocus.event;185186private _onDidChangeViewModel = this._register(new Emitter<IChatWidgetViewModelChangeEvent>());187readonly onDidChangeViewModel = this._onDidChangeViewModel.event;188189private _onDidScroll = this._register(new Emitter<void>());190readonly onDidScroll = this._onDidScroll.event;191192private _onDidAcceptInput = this._register(new Emitter<void>());193readonly onDidAcceptInput = this._onDidAcceptInput.event;194195private _onDidHide = this._register(new Emitter<void>());196readonly onDidHide = this._onDidHide.event;197198private _onDidShow = this._register(new Emitter<void>());199readonly onDidShow = this._onDidShow.event;200201private _onDidChangeParsedInput = this._register(new Emitter<void>());202readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event;203204private readonly _onWillMaybeChangeHeight = new Emitter<void>();205readonly onWillMaybeChangeHeight: Event<void> = this._onWillMaybeChangeHeight.event;206207private _onDidChangeHeight = this._register(new Emitter<number>());208readonly onDidChangeHeight = this._onDidChangeHeight.event;209210private readonly _onDidChangeContentHeight = new Emitter<void>();211readonly onDidChangeContentHeight: Event<void> = this._onDidChangeContentHeight.event;212213private _onDidChangeEmptyState = this._register(new Emitter<void>());214readonly onDidChangeEmptyState = this._onDidChangeEmptyState.event;215216contribs: ReadonlyArray<IChatWidgetContrib> = [];217218private listContainer!: HTMLElement;219private container!: HTMLElement;220221get domNode() { return this.container; }222223private tree!: WorkbenchObjectTree<ChatTreeItem, FuzzyScore>;224private renderer!: ChatListItemRenderer;225private readonly _codeBlockModelCollection: CodeBlockModelCollection;226private lastItem: ChatTreeItem | undefined;227228private readonly visibilityTimeoutDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());229private readonly visibilityAnimationFrameDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());230private readonly scrollAnimationFrameDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());231232private readonly inputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());233private readonly inlineInputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());234private inputContainer!: HTMLElement;235private focusedInputDOM!: HTMLElement;236private editorOptions!: ChatEditorOptions;237238private recentlyRestoredCheckpoint: boolean = false;239240private settingChangeCounter = 0;241242private welcomeMessageContainer!: HTMLElement;243private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());244245private readonly chatSuggestNextWidget: ChatSuggestNextWidget;246247private bodyDimension: dom.Dimension | undefined;248private visibleChangeCount = 0;249private requestInProgress: IContextKey<boolean>;250private agentInInput: IContextKey<boolean>;251private currentRequest: Promise<void> | undefined;252253private _visible = false;254get visible() { return this._visible; }255256private previousTreeScrollHeight: number = 0;257258/**259* Whether the list is scroll-locked to the bottom. Initialize to true so that we can scroll to the bottom on first render.260* The initial render leads to a lot of `onDidChangeTreeContentHeight` as the renderer works out the real heights of rows.261*/262private scrollLock = true;263264private _instructionFilesCheckPromise: Promise<boolean> | undefined;265private _instructionFilesExist: boolean | undefined;266267private _isRenderingWelcome = false;268269// Coding agent locking state270private _lockedAgent?: {271id: string;272name: string;273prefix: string;274displayName: string;275};276private readonly _lockedToCodingAgentContextKey: IContextKey<boolean>;277private readonly _agentSupportsAttachmentsContextKey: IContextKey<boolean>;278private readonly _sessionIsEmptyContextKey: IContextKey<boolean>;279private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments;280281// Cache for prompt file descriptions to avoid async calls during rendering282private readonly promptDescriptionsCache = new Map<string, string>();283private readonly promptUriCache = new Map<string, URI>();284private _isLoadingPromptDescriptions = false;285286private _mostRecentlyFocusedItemIndex: number = -1;287288private readonly viewModelDisposables = this._register(new DisposableStore());289private _viewModel: ChatViewModel | undefined;290291private set viewModel(viewModel: ChatViewModel | undefined) {292if (this._viewModel === viewModel) {293return;294}295296const previousSessionResource = this._viewModel?.sessionResource;297this.viewModelDisposables.clear();298299this._viewModel = viewModel;300if (viewModel) {301this.viewModelDisposables.add(viewModel);302this.logService.debug('ChatWidget#setViewModel: have viewModel');303304// If switching to a model with a request in progress, play progress sound305if (viewModel.model.requestInProgress.get()) {306this.chatAccessibilityService.acceptRequest(viewModel.sessionResource, true);307}308} else {309this.logService.debug('ChatWidget#setViewModel: no viewModel');310}311312this.currentRequest = undefined;313this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource });314}315316get viewModel() {317return this._viewModel;318}319320private readonly _editingSession = observableValue<IChatEditingSession | undefined>(this, undefined);321322private parsedChatRequest: IParsedChatRequest | undefined;323get parsedInput() {324if (this.parsedChatRequest === undefined) {325if (!this.viewModel) {326return { text: '', parts: [] };327}328329this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser)330.parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, {331selectedAgent: this._lastSelectedAgent,332mode: this.input.currentModeKind,333forcedAgent: this._lockedAgent?.id ? this.chatAgentService.getAgent(this._lockedAgent.id) : undefined334});335this._onDidChangeParsedInput.fire();336}337338return this.parsedChatRequest;339}340341get scopedContextKeyService(): IContextKeyService {342return this.contextKeyService;343}344345private readonly _location: IChatWidgetLocationOptions;346get location() {347return this._location.location;348}349350readonly viewContext: IChatWidgetViewContext;351352get supportsChangingModes(): boolean {353return !!this.viewOptions.supportsChangingModes;354}355356get locationData() {357return this._location.resolveData?.();358}359360constructor(361location: ChatAgentLocation | IChatWidgetLocationOptions,362viewContext: IChatWidgetViewContext | undefined,363private readonly viewOptions: IChatWidgetViewOptions,364private readonly styles: IChatWidgetStyles,365@ICodeEditorService private readonly codeEditorService: ICodeEditorService,366@IEditorService private readonly editorService: IEditorService,367@IConfigurationService private readonly configurationService: IConfigurationService,368@IContextKeyService private readonly contextKeyService: IContextKeyService,369@IInstantiationService private readonly instantiationService: IInstantiationService,370@IChatService private readonly chatService: IChatService,371@IChatAgentService private readonly chatAgentService: IChatAgentService,372@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,373@IContextMenuService private readonly contextMenuService: IContextMenuService,374@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService,375@ILogService private readonly logService: ILogService,376@IThemeService private readonly themeService: IThemeService,377@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,378@IChatEditingService chatEditingService: IChatEditingService,379@ITelemetryService private readonly telemetryService: ITelemetryService,380@IPromptsService private readonly promptsService: IPromptsService,381@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,382@IChatModeService private readonly chatModeService: IChatModeService,383@IChatLayoutService private readonly chatLayoutService: IChatLayoutService,384@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,385@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,386@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,387@IChatTodoListService private readonly chatTodoListService: IChatTodoListService,388@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,389@ILifecycleService private readonly lifecycleService: ILifecycleService390) {391super();392393this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);394this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService);395this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService);396397this.viewContext = viewContext ?? {};398399const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel);400401if (typeof location === 'object') {402this._location = location;403} else {404this._location = { location };405}406407ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true);408ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location);409ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this));410this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService);411this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService);412413this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded()));414415this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => {416const currentSession = this._editingSession.read(reader);417if (!currentSession) {418return;419}420const entries = currentSession.entries.read(reader);421const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified);422return decidedEntries.map(entry => entry.entryId);423}));424this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => {425const currentSession = this._editingSession.read(reader);426const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here427const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified);428return decidedEntries.length > 0;429}));430this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => {431const currentSession = this._editingSession.read(reader);432if (!currentSession) {433return false;434}435const entries = currentSession.entries.read(reader);436return entries.length > 0;437}));438this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => {439return this._editingSession.read(reader) !== null;440}));441this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => {442return this._editingSession.read(r)?.canUndo.read(r) || false;443}));444this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => {445return this._editingSession.read(r)?.canRedo.read(r) || false;446}));447this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => {448const chatModel = viewModelObs.read(r)?.model;449const editingSession = this._editingSession.read(r);450if (!editingSession || !chatModel) {451return false;452}453const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r);454return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete;455}));456457this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection, undefined));458this.chatSuggestNextWidget = this._register(this.instantiationService.createInstance(ChatSuggestNextWidget));459460this._register(this.configurationService.onDidChangeConfiguration((e) => {461if (e.affectsConfiguration('chat.renderRelatedFiles')) {462this.input.renderChatRelatedFiles();463}464465if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) {466this.settingChangeCounter++;467this.onDidChangeItems();468}469}));470471this._register(autorun(r => {472const viewModel = viewModelObs.read(r);473const sessions = chatEditingService.editingSessionsObs.read(r);474475const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, viewModel?.sessionResource));476this._editingSession.set(undefined, undefined);477this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc.478479if (!session) {480// none or for a different chat widget481return;482}483484const entries = session.entries.read(r);485for (const entry of entries) {486entry.state.read(r); // SIGNAL487}488489this._editingSession.set(session, undefined);490491r.store.add(session.onDidDispose(() => {492this._editingSession.set(undefined, undefined);493this.renderChatEditingSessionState();494}));495r.store.add(this.inputEditor.onDidChangeModelContent(() => {496if (this.getInput() === '') {497this.refreshParsedInput();498}499}));500this.renderChatEditingSessionState();501}));502503this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise<ICodeEditor | null> => {504const resource = input.resource;505if (resource.scheme !== Schemas.vscodeChatCodeBlock) {506return null;507}508509const responseId = resource.path.split('/').at(1);510if (!responseId) {511return null;512}513514const item = this.viewModel?.getItems().find(item => item.id === responseId);515if (!item) {516return null;517}518519// TODO: needs to reveal the chat view520521this.reveal(item);522523await timeout(0); // wait for list to actually render524525for (const codeBlockPart of this.renderer.editorsInUse()) {526if (extUri.isEqual(codeBlockPart.uri, resource, true)) {527const editor = codeBlockPart.editor;528529let relativeTop = 0;530const editorDomNode = editor.getDomNode();531if (editorDomNode) {532const row = dom.findParentWithClass(editorDomNode, 'monaco-list-row');533if (row) {534relativeTop = dom.getTopLeftOffset(editorDomNode).top - dom.getTopLeftOffset(row).top;535}536}537538if (input.options?.selection) {539const editorSelectionTopOffset = editor.getTopForPosition(input.options.selection.startLineNumber, input.options.selection.startColumn);540relativeTop += editorSelectionTopOffset;541542editor.focus();543editor.setSelection({544startLineNumber: input.options.selection.startLineNumber,545startColumn: input.options.selection.startColumn,546endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber,547endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn548});549}550551this.reveal(item, relativeTop);552553return editor;554}555}556return null;557}));558559this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext()));560561this._register(this.chatTodoListService.onDidUpdateTodos((sessionResource) => {562if (isEqual(this.viewModel?.sessionResource, sessionResource)) {563this.inputPart.renderChatTodoListWidget(sessionResource);564}565}));566}567568private _lastSelectedAgent: IChatAgentData | undefined;569set lastSelectedAgent(agent: IChatAgentData | undefined) {570this.parsedChatRequest = undefined;571this._lastSelectedAgent = agent;572this._updateAgentCapabilitiesContextKeys(agent);573this._onDidChangeParsedInput.fire();574}575576get lastSelectedAgent(): IChatAgentData | undefined {577return this._lastSelectedAgent;578}579580private _updateAgentCapabilitiesContextKeys(agent: IChatAgentData | undefined): void {581// Check if the agent has capabilities defined directly582const capabilities = agent?.capabilities ?? (this._lockedAgent ? this.chatSessionsService.getCapabilitiesForSessionType(this._lockedAgent.id) : undefined);583this._attachmentCapabilities = capabilities ?? supportsAllAttachments;584585const supportsAttachments = Object.keys(filter(this._attachmentCapabilities, (key, value) => value === true)).length > 0;586this._agentSupportsAttachmentsContextKey.set(supportsAttachments);587}588589get supportsFileReferences(): boolean {590return !!this.viewOptions.supportsFileReferences;591}592593get attachmentCapabilities(): IChatAgentAttachmentCapabilities {594return this._attachmentCapabilities;595}596597get input(): ChatInputPart {598return this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart;599}600601private get inputPart(): ChatInputPart {602return this.inputPartDisposable.value!;603}604605private get inlineInputPart(): ChatInputPart {606return this.inlineInputPartDisposable.value!;607}608609get inputEditor(): ICodeEditor {610return this.input.inputEditor;611}612613get contentHeight(): number {614return this.input.contentHeight + this.tree.contentHeight + this.chatSuggestNextWidget.height;615}616617get attachmentModel(): ChatAttachmentModel {618return this.input.attachmentModel;619}620621render(parent: HTMLElement): void {622const viewId = isIChatViewViewContext(this.viewContext) ? this.viewContext.viewId : undefined;623this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground));624const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false;625const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop;626const renderStyle = this.viewOptions.renderStyle;627const renderInputToolbarBelowInput = this.viewOptions.renderInputToolbarBelowInput ?? false;628629this.container = dom.append(parent, $('.interactive-session'));630this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' }));631this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput()));632633this._register(this.chatSuggestNextWidget.onDidChangeHeight(() => {634if (this.bodyDimension) {635this.layout(this.bodyDimension.height, this.bodyDimension.width);636}637}));638this._register(this.chatSuggestNextWidget.onDidSelectPrompt(({ handoff, agentId }) => {639this.handleNextPromptSelection(handoff, agentId);640}));641642if (renderInputOnTop) {643this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });644this.listContainer = dom.append(this.container, $(`.interactive-list`));645} else {646this.listContainer = dom.append(this.container, $(`.interactive-list`));647dom.append(this.container, this.chatSuggestNextWidget.domNode);648this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });649}650651this.renderWelcomeViewContentIfNeeded();652this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle });653654const scrollDownButton = this._register(new Button(this.listContainer, {655supportIcons: true,656buttonBackground: asCssVariable(buttonSecondaryBackground),657buttonForeground: asCssVariable(buttonSecondaryForeground),658buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground),659}));660scrollDownButton.element.classList.add('chat-scroll-down');661scrollDownButton.label = `$(${Codicon.chevronDown.id})`;662scrollDownButton.setTitle(localize('scrollDownButtonLabel', "Scroll down"));663this._register(scrollDownButton.onDidClick(() => {664this.scrollLock = true;665this.scrollToEnd();666}));667668// Update the font family and size669this._register(autorun(reader => {670const fontFamily = this.chatLayoutService.fontFamily.read(reader);671const fontSize = this.chatLayoutService.fontSize.read(reader);672673this.container.style.setProperty('--vscode-chat-font-family', fontFamily);674this.container.style.fontSize = `${fontSize}px`;675676if (this.visible) {677this.tree.rerender();678}679}));680681this._register(Event.runAndSubscribe(this.editorOptions.onDidChange, () => this.onDidStyleChange()));682683// Do initial render684if (this.viewModel) {685this.onDidChangeItems();686this.scrollToEnd();687}688689this.contribs = ChatWidget.CONTRIBS.map(contrib => {690try {691return this._register(this.instantiationService.createInstance(contrib, this));692} catch (err) {693this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err));694return undefined;695}696}).filter(isDefined);697698this._register(this.chatWidgetService.register(this));699700const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput);701this._register(autorun(r => {702const input = parsedInput.read(r);703704const newPromptAttachments = new Map<string, IChatRequestVariableEntry>();705const oldPromptAttachments = new Set<string>();706707// get all attachments, know those that are prompt-referenced708for (const attachment of this.attachmentModel.attachments) {709if (attachment.range) {710oldPromptAttachments.add(attachment.id);711}712}713714// update/insert prompt-referenced attachments715for (const part of input.parts) {716if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) {717const entry = part.toVariableEntry();718newPromptAttachments.set(entry.id, entry);719oldPromptAttachments.delete(entry.id);720}721}722723this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values());724}));725726if (!this.focusedInputDOM) {727this.focusedInputDOM = this.container.appendChild(dom.$('.focused-input-dom'));728}729}730731private scrollToEnd() {732if (this.lastItem) {733const offset = Math.max(this.lastItem.currentRenderedHeight ?? 0, 1e6);734if (this.tree.hasElement(this.lastItem)) {735this.tree.reveal(this.lastItem, offset);736}737}738}739740focusInput(): void {741this.input.focus();742743// Sometimes focusing the input part is not possible,744// but we'd like to be the last focused chat widget,745// so we emit an optimistic onDidFocus event nonetheless.746this._onDidFocus.fire();747}748749hasInputFocus(): boolean {750return this.input.hasFocus();751}752753refreshParsedInput() {754if (!this.viewModel) {755return;756}757this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind });758this._onDidChangeParsedInput.fire();759}760761getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined {762if (!isResponseVM(item)) {763return;764}765const items = this.viewModel?.getItems();766if (!items) {767return;768}769const responseItems = items.filter(i => isResponseVM(i));770const targetIndex = responseItems.indexOf(item);771if (targetIndex === undefined) {772return;773}774const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1;775if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) {776return;777}778return responseItems[indexToFocus];779}780781async clear(): Promise<void> {782this.logService.debug('ChatWidget#clear');783if (this._dynamicMessageLayoutData) {784this._dynamicMessageLayoutData.enabled = true;785}786787if (this.viewModel?.editing) {788this.finishedEditing();789}790791if (this.viewModel) {792this.viewModel.resetInputPlaceholder();793}794if (this._lockedAgent) {795this.lockToCodingAgent(this._lockedAgent.name, this._lockedAgent.displayName, this._lockedAgent.id);796} else {797this.unlockFromCodingAgent();798}799800this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);801this.chatSuggestNextWidget.hide();802await this.viewOptions.clear?.();803}804805private onDidChangeItems(skipDynamicLayout?: boolean) {806if (this._visible || !this.viewModel) {807const treeItems = (this.viewModel?.getItems() ?? [])808.map((item): ITreeElement<ChatTreeItem> => {809return {810element: item,811collapsed: false,812collapsible: false813};814});815816817if (treeItems.length > 0) {818this.updateChatViewVisibility();819} else {820this.renderWelcomeViewContentIfNeeded();821}822823this._onWillMaybeChangeHeight.fire();824825this.lastItem = treeItems.at(-1)?.element;826ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set(this.lastItem ? [this.lastItem.id] : []);827this.tree.setChildren(null, treeItems, {828diffIdentityProvider: {829getId: (element) => {830return element.dataId +831// Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied.832`${(isRequestVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` +833// If a response is in the process of progressive rendering, we need to ensure that it will834// be re-rendered so progressive rendering is restarted, even if the model wasn't updated.835`${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` +836// Re-render once content references are loaded837(isResponseVM(element) ? `_${element.contentReferences.length}` : '') +838// Re-render if element becomes hidden due to undo/redo839`_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` +840// Re-render if we have an element currently being edited841`_${this.viewModel?.editing ? '1' : '0'}` +842// Re-render if we have an element currently being checkpointed843`_${this.viewModel?.model.checkpoint ? '1' : '0'}` +844// Re-render all if invoked by setting change845`_setting${this.settingChangeCounter || '0'}` +846// Rerender request if we got new content references in the response847// since this may change how we render the corresponding attachments in the request848(isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : '');849},850}851});852853if (!skipDynamicLayout && this._dynamicMessageLayoutData) {854this.layoutDynamicChatTreeItemMode();855}856857this.renderFollowups();858}859}860861/**862* Updates the DOM visibility of welcome view and chat list immediately863*/864private updateChatViewVisibility(): void {865if (!this.viewModel) {866return;867}868869const numItems = this.viewModel.getItems().length;870dom.setVisibility(numItems === 0, this.welcomeMessageContainer);871dom.setVisibility(numItems !== 0, this.listContainer);872873this._onDidChangeEmptyState.fire();874}875876isEmpty(): boolean {877return (this.viewModel?.getItems().length ?? 0) === 0;878}879880/**881* Renders the welcome view content when needed.882*/883private renderWelcomeViewContentIfNeeded() {884if (this._isRenderingWelcome) {885return;886}887888this._isRenderingWelcome = true;889try {890if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal' || this.lifecycleService.willShutdown) {891return;892}893894const numItems = this.viewModel?.getItems().length ?? 0;895if (!numItems) {896const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind);897let additionalMessage: string | IMarkdownString | undefined;898if (this.chatEntitlementService.anonymous && !this.chatEntitlementService.sentiment.installed) {899const providers = product.defaultChatAgent.provider;900additionalMessage = 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 });901} else {902additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage;903}904if (!additionalMessage && !this._lockedAgent) {905additionalMessage = this._getGenerateInstructionsMessage();906}907const welcomeContent = this.getWelcomeViewContent(additionalMessage);908if (!this.welcomePart.value || this.welcomePart.value.needsRerender(welcomeContent)) {909dom.clearNode(this.welcomeMessageContainer);910911this.welcomePart.value = this.instantiationService.createInstance(912ChatViewWelcomePart,913welcomeContent,914{915location: this.location,916isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent917}918);919dom.append(this.welcomeMessageContainer, this.welcomePart.value.element);920}921}922923this.updateChatViewVisibility();924} finally {925this._isRenderingWelcome = false;926}927}928929private _getGenerateInstructionsMessage(): IMarkdownString {930// Start checking for instruction files immediately if not already done931if (!this._instructionFilesCheckPromise) {932this._instructionFilesCheckPromise = this._checkForAgentInstructionFiles();933// Use VS Code's idiomatic pattern for disposal-safe promise callbacks934this._register(thenIfNotDisposed(this._instructionFilesCheckPromise, hasFiles => {935this._instructionFilesExist = hasFiles;936// Only re-render if the current view still doesn't have items and we're showing the welcome message937const hasViewModelItems = this.viewModel?.getItems().length ?? 0;938if (hasViewModelItems === 0) {939this.renderWelcomeViewContentIfNeeded();940}941}));942}943944// If we already know the result, use it945if (this._instructionFilesExist === true) {946// Don't show generate instructions message if files exist947return new MarkdownString('');948} else if (this._instructionFilesExist === false) {949// Show generate instructions message if no files exist950const generateInstructionsCommand = 'workbench.action.chat.generateInstructions';951return new MarkdownString(localize(952'chatWidget.instructions',953"[Generate Agent Instructions]({0}) to onboard AI onto your codebase.",954`command:${generateInstructionsCommand}`955), { isTrusted: { enabledCommands: [generateInstructionsCommand] } });956}957958// While checking, don't show the generate instructions message959return new MarkdownString('');960}961962/**963* Checks if any agent instruction files (.github/copilot-instructions.md or AGENTS.md) exist in the workspace.964* Used to determine whether to show the "Generate Agent Instructions" hint.965*966* @returns true if instruction files exist OR if instruction features are disabled (to hide the hint)967*/968private async _checkForAgentInstructionFiles(): Promise<boolean> {969try {970const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES);971const useAgentMd = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD);972if (!useCopilotInstructionsFiles && !useAgentMd) {973// If both settings are disabled, return true to hide the hint (since the features aren't enabled)974return true;975}976return (977(await this.promptsService.listCopilotInstructionsMDs(CancellationToken.None)).length > 0 ||978// Note: only checking for AGENTS.md files at the root folder, not ones in subfolders.979(await this.promptsService.listAgentMDs(CancellationToken.None, false)).length > 0980);981} catch (error) {982// On error, assume no instruction files exist to be safe983this.logService.warn('[ChatWidget] Error checking for instruction files:', error);984return false;985}986}987988private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent {989if (this.isLockedToCodingAgent) {990// Check for provider-specific customizations from chat sessions service991const providerIcon = this._lockedAgent ? this.chatSessionsService.getIconForSessionType(this._lockedAgent.id) : undefined;992const providerTitle = this._lockedAgent ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgent.id) : undefined;993const providerMessage = this._lockedAgent ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgent.id) : undefined;994995// Fallback to default messages if provider doesn't specify996const message = providerMessage997? new MarkdownString(providerMessage)998: (this._lockedAgent?.prefix === '@copilot '999? 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 })1000: 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));10011002return {1003title: providerTitle ?? localize('codingAgentTitle', "Delegate to {0}", this._lockedAgent?.prefix),1004message,1005icon: providerIcon ?? Codicon.sendToRemoteAgent,1006additionalMessage,1007useLargeIcon: !!providerIcon,1008};1009}10101011let title: string;1012if (this.input.currentModeKind === ChatModeKind.Ask) {1013title = localize('chatDescription', "Ask about your code");1014} else if (this.input.currentModeKind === ChatModeKind.Edit) {1015title = localize('editsTitle', "Edit in context");1016} else {1017title = localize('agentTitle', "Build with Agent");1018}10191020return {1021title,1022message: new MarkdownString(DISCLAIMER),1023icon: Codicon.chatSparkle,1024additionalMessage,1025suggestedPrompts: this.getPromptFileSuggestions()1026};1027}10281029private getPromptFileSuggestions(): IChatSuggestedPrompts[] {10301031// Use predefined suggestions for new users1032if (!this.chatEntitlementService.sentiment.installed) {1033const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY;1034if (isEmpty) {1035return [1036{1037icon: Codicon.vscode,1038label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"),1039prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"),1040},1041{1042icon: Codicon.newFolder,1043label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"),1044prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"),1045}1046];1047} else {1048return [1049{1050icon: Codicon.debugAlt,1051label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"),1052prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"),1053},1054{1055icon: Codicon.gear,1056label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"),1057prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"),1058}1059];1060}1061}10621063// Get the current workspace folder context if available1064const activeEditor = this.editorService.activeEditor;1065const resource = activeEditor ? EditorResourceAccessor.getOriginalUri(activeEditor) : undefined;10661067// Get the prompt file suggestions configuration1068const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService, resource);1069if (!suggestions) {1070return [];1071}10721073const result: IChatSuggestedPrompts[] = [];1074const promptsToLoad: string[] = [];10751076// First, collect all prompts that need loading (regardless of shouldInclude)1077for (const [promptName] of Object.entries(suggestions)) {1078const description = this.promptDescriptionsCache.get(promptName);1079if (description === undefined) {1080promptsToLoad.push(promptName);1081}1082}10831084// If we have prompts to load, load them asynchronously and don't return anything yet1085// But only if we're not already loading to prevent infinite loop1086if (promptsToLoad.length > 0 && !this._isLoadingPromptDescriptions) {1087this.loadPromptDescriptions(promptsToLoad);1088return [];1089}10901091// Now process the suggestions with loaded descriptions1092const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = [];10931094for (const [promptName, condition] of Object.entries(suggestions)) {1095let score = 0;10961097// Handle boolean conditions1098if (typeof condition === 'boolean') {1099score = condition ? 1 : 0;1100}1101// Handle when clause conditions1102else if (typeof condition === 'string') {1103try {1104const whenClause = ContextKeyExpr.deserialize(condition);1105if (whenClause) {1106// Test against all open code editors1107const allEditors = this.codeEditorService.listCodeEditors();11081109if (allEditors.length > 0) {1110// Count how many editors match the when clause1111score = allEditors.reduce((count, editor) => {1112try {1113const editorContext = this.contextKeyService.getContext(editor.getDomNode());1114return count + (whenClause.evaluate(editorContext) ? 1 : 0);1115} catch (error) {1116// Log error for this specific editor but continue with others1117this.logService.warn('Failed to evaluate when clause for editor:', error);1118return count;1119}1120}, 0);1121} else {1122// Fallback to global context if no editors are open1123score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0;1124}1125} else {1126score = 0;1127}1128} catch (error) {1129// Log the error but don't fail completely1130this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error);1131score = 0;1132}1133}11341135if (score > 0) {1136promptsWithScores.push({ promptName, condition, score });1137}1138}11391140// Sort by score (descending) and take top 51141promptsWithScores.sort((a, b) => b.score - a.score);1142const topPrompts = promptsWithScores.slice(0, 5);11431144// Build the final result array1145for (const { promptName } of topPrompts) {1146const description = this.promptDescriptionsCache.get(promptName);1147const commandLabel = localize('chatWidget.promptFile.commandLabel', "{0}", promptName);1148const uri = this.promptUriCache.get(promptName);1149const descriptionText = description?.trim() ? description : undefined;1150result.push({1151icon: Codicon.run,1152label: commandLabel,1153description: descriptionText,1154prompt: `/${promptName} `,1155uri: uri1156});1157}11581159return result;1160}11611162private async loadPromptDescriptions(promptNames: string[]): Promise<void> {1163// Don't start loading if the widget is being disposed1164if (this._store.isDisposed) {1165return;1166}11671168// Set loading guard to prevent infinite loop1169this._isLoadingPromptDescriptions = true;1170try {1171// Get all available prompt files with their metadata1172const promptCommands = await this.promptsService.getPromptSlashCommands(CancellationToken.None);11731174let cacheUpdated = false;1175// Load descriptions only for the specified prompts1176for (const promptCommand of promptCommands) {1177if (promptNames.includes(promptCommand.name)) {1178const description = promptCommand.description;1179if (description) {1180this.promptDescriptionsCache.set(promptCommand.name, description);1181cacheUpdated = true;1182} else {1183// Set empty string to indicate we've checked this prompt1184this.promptDescriptionsCache.set(promptCommand.name, '');1185cacheUpdated = true;1186}1187}1188}11891190// Fire event to trigger a re-render of the welcome view only if cache was updated1191if (cacheUpdated) {1192this.renderWelcomeViewContentIfNeeded();1193}1194} catch (error) {1195this.logService.warn('Failed to load specific prompt descriptions:', error);1196} finally {1197// Always clear the loading guard, even on error1198this._isLoadingPromptDescriptions = false;1199}1200}12011202private async renderChatEditingSessionState() {1203if (!this.input) {1204return;1205}1206this.input.renderChatEditingSessionState(this._editingSession.get() ?? null);1207}12081209private async renderFollowups(): Promise<void> {1210if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) {1211this.input.renderFollowups(this.lastItem.replyFollowups, this.lastItem);1212} else {1213this.input.renderFollowups(undefined, undefined);1214}12151216if (this.bodyDimension) {1217this.layout(this.bodyDimension.height, this.bodyDimension.width);1218}1219}12201221private renderChatSuggestNextWidget(): void {1222if (this.lifecycleService.willShutdown) {1223return;1224}12251226// Skip rendering in coding agent sessions1227if (this.isLockedToCodingAgent) {1228this.chatSuggestNextWidget.hide();1229return;1230}12311232const items = this.viewModel?.getItems() ?? [];1233if (!items.length) {1234return;1235}12361237const lastItem = items[items.length - 1];1238const lastResponseComplete = lastItem && isResponseVM(lastItem) && lastItem.isComplete;1239if (!lastResponseComplete) {1240return;1241}1242// Get the currently selected mode directly from the observable1243// Note: We use currentModeObs instead of currentModeKind because currentModeKind returns1244// the ChatModeKind enum (e.g., 'agent'), which doesn't distinguish between custom modes.1245// Custom modes all have kind='agent' but different IDs.1246const currentMode = this.input.currentModeObs.get();1247const handoffs = currentMode?.handOffs?.get();12481249// Only show if: mode has handoffs AND chat has content AND not quick chat1250const shouldShow = currentMode && handoffs && handoffs.length > 0;12511252if (shouldShow) {1253// Log telemetry only when widget transitions from hidden to visible1254const wasHidden = this.chatSuggestNextWidget.domNode.style.display === 'none';1255this.chatSuggestNextWidget.render(currentMode);12561257if (wasHidden) {1258this.telemetryService.publicLog2<ChatHandoffWidgetShownEvent, ChatHandoffWidgetShownClassification>('chat.handoffWidgetShown', {1259agent: currentMode.id,1260handoffCount: handoffs.length1261});1262}1263} else {1264this.chatSuggestNextWidget.hide();1265}12661267// Trigger layout update1268if (this.bodyDimension) {1269this.layout(this.bodyDimension.height, this.bodyDimension.width);1270}1271}12721273private handleNextPromptSelection(handoff: IHandOff, agentId?: string): void {1274// Hide the widget after selection1275this.chatSuggestNextWidget.hide();12761277const promptToUse = handoff.prompt;12781279// Log telemetry1280const currentMode = this.input.currentModeObs.get();1281const fromAgent = currentMode?.id ?? '';1282this.telemetryService.publicLog2<ChatHandoffClickEvent, ChatHandoffClickClassification>('chat.handoffClicked', {1283fromAgent: fromAgent,1284toAgent: agentId || handoff.agent || '',1285hasPrompt: Boolean(promptToUse),1286autoSend: Boolean(handoff.send)1287});12881289// If agentId is provided (from chevron dropdown), delegate to that chat session1290// Otherwise, switch to the handoff agent1291if (agentId) {1292// Delegate to chat session (e.g., @background or @cloud)1293this.input.setValue(`@${agentId} ${promptToUse}`, false);1294this.input.focus();1295// Auto-submit for delegated chat sessions1296this.acceptInput().catch(e => this.logService.error('Failed to handle handoff continueOn', e));1297} else if (handoff.agent) {1298// Regular handoff to specified agent1299this._switchToAgentByName(handoff.agent);1300// Insert the handoff prompt into the input1301this.input.setValue(promptToUse, false);1302this.input.focus();13031304// Auto-submit if send flag is true1305if (handoff.send) {1306this.acceptInput();1307}1308}1309}13101311async handleDelegationExitIfNeeded(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): Promise<void> {1312if (!this._shouldExitAfterDelegation(sourceAgent, targetAgent)) {1313return;1314}13151316try {1317await this._handleDelegationExit();1318} catch (e) {1319this.logService.error('Failed to handle delegation exit', e);1320}1321}13221323private _shouldExitAfterDelegation(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): boolean {1324if (!targetAgent) {1325// Undefined behavior1326return false;1327}13281329if (!this.configurationService.getValue<boolean>(ChatConfiguration.ExitAfterDelegation)) {1330return false;1331}13321333// Never exit if the source and target are the same (that means that you're providing a follow up, etc.)1334// NOTE: sourceAgent would be the chatWidget's 'lockedAgent'1335if (sourceAgent && sourceAgent.id === targetAgent.id) {1336return false;1337}13381339if (!isIChatViewViewContext(this.viewContext)) {1340return false;1341}13421343const contribution = this.chatSessionsService.getChatSessionContribution(targetAgent.id);1344if (!contribution) {1345return false;1346}13471348if (contribution.canDelegate !== true) {1349return false;1350}13511352return true;1353}13541355/**1356* Handles the exit of the panel chat when a delegation to another session occurs.1357* Waits for the response to complete and any pending confirmations to be resolved,1358* then clears the widget unless the final message is an error.1359*/1360private async _handleDelegationExit(): Promise<void> {1361const viewModel = this.viewModel;1362if (!viewModel) {1363return;1364}13651366const parentSessionResource = viewModel.sessionResource;13671368// Check if response is complete, not pending confirmation, and has no error1369const checkIfShouldClear = (): boolean => {1370const items = viewModel.getItems();1371const lastItem = items[items.length - 1];1372if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {1373const hasError = Boolean(lastItem.result?.errorDetails);1374return !hasError;1375}1376return false;1377};13781379if (checkIfShouldClear()) {1380await this.clear();1381this.archiveLocalParentSession(parentSessionResource);1382return;1383}13841385const shouldClear = await new Promise<boolean>(resolve => {1386const disposable = viewModel.onDidChange(() => {1387const result = checkIfShouldClear();1388if (result) {1389cleanup();1390resolve(true);1391}1392});1393const timeout = setTimeout(() => {1394cleanup();1395resolve(false);1396}, 30_000); // 30 second timeout1397const cleanup = () => {1398clearTimeout(timeout);1399disposable.dispose();1400};1401});14021403if (shouldClear) {1404await this.clear();1405this.archiveLocalParentSession(parentSessionResource);1406}1407}14081409private async archiveLocalParentSession(sessionResource: URI): Promise<void> {1410if (sessionResource.scheme !== Schemas.vscodeLocalChatSession) {1411return;1412}14131414// Implicitly keep parent session's changes as they've now been delegated to the new agent.1415await this.chatService.getSession(sessionResource)?.editingSession?.accept();14161417const session = this.agentSessionsService.getSession(sessionResource);1418session?.setArchived(true);1419}14201421setVisible(visible: boolean): void {1422const wasVisible = this._visible;1423this._visible = visible;1424this.visibleChangeCount++;1425this.renderer.setVisible(visible);1426this.input.setVisible(visible);14271428if (visible) {1429if (!wasVisible) {1430this.visibilityTimeoutDisposable.value = disposableTimeout(() => {1431// Progressive rendering paused while hidden, so start it up again.1432// Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here)1433if (this._visible) {1434this.onDidChangeItems(true);1435}1436}, 0);14371438this.visibilityAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {1439this._onDidShow.fire();1440});1441}1442} else if (wasVisible) {1443this._onDidHide.fire();1444}1445}14461447private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void {1448const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])));1449const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200);1450const rendererDelegate: IChatRendererDelegate = {1451getListLength: () => this.tree.getNode(null).visibleChildrenCount,1452onDidScroll: this.onDidScroll,1453container: listContainer,1454currentChatMode: () => this.input.currentModeKind,1455};14561457// Create a dom element to hold UI from editor widgets embedded in chat messages1458const overflowWidgetsContainer = document.createElement('div');1459overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor');1460listContainer.append(overflowWidgetsContainer);14611462this.renderer = this._register(scopedInstantiationService.createInstance(1463ChatListItemRenderer,1464this.editorOptions,1465options,1466rendererDelegate,1467this._codeBlockModelCollection,1468overflowWidgetsContainer,1469this.viewModel,1470));14711472this._register(this.renderer.onDidClickRequest(async item => {1473this.clickedRequest(item);1474}));14751476this._register(this.renderer.onDidRerender(item => {1477if (isRequestVM(item.currentElement) && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1478if (!item.rowContainer.contains(this.inputContainer)) {1479item.rowContainer.appendChild(this.inputContainer);1480}1481this.input.focus();1482}1483}));14841485this._register(this.renderer.onDidDispose((item) => {1486this.focusedInputDOM.appendChild(this.inputContainer);1487this.input.focus();1488}));14891490this._register(this.renderer.onDidFocusOutside(() => {1491this.finishedEditing();1492}));14931494this._register(this.renderer.onDidClickFollowup(item => {1495// is this used anymore?1496this.acceptInput(item.message);1497}));1498this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(e => {1499const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId);1500if (request) {1501const options: IChatSendRequestOptions = {1502noCommandDetection: true,1503attempt: request.attempt + 1,1504location: this.location,1505userSelectedModelId: this.input.currentLanguageModel,1506modeInfo: this.input.currentModeInfo,1507};1508this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e));1509}1510}));15111512this.tree = this._register(scopedInstantiationService.createInstance(1513WorkbenchObjectTree<ChatTreeItem, FuzzyScore>,1514'Chat',1515listContainer,1516delegate,1517[this.renderer],1518{1519identityProvider: { getId: (e: ChatTreeItem) => e.id },1520horizontalScrolling: false,1521alwaysConsumeMouseWheel: false,1522supportDynamicHeights: true,1523hideTwistiesOfChildlessElements: true,1524accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider),1525keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO1526setRowLineHeight: false,1527filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined,1528scrollToActiveElement: true,1529overrideStyles: {1530listFocusBackground: this.styles.listBackground,1531listInactiveFocusBackground: this.styles.listBackground,1532listActiveSelectionBackground: this.styles.listBackground,1533listFocusAndSelectionBackground: this.styles.listBackground,1534listInactiveSelectionBackground: this.styles.listBackground,1535listHoverBackground: this.styles.listBackground,1536listBackground: this.styles.listBackground,1537listFocusForeground: this.styles.listForeground,1538listHoverForeground: this.styles.listForeground,1539listInactiveFocusForeground: this.styles.listForeground,1540listInactiveSelectionForeground: this.styles.listForeground,1541listActiveSelectionForeground: this.styles.listForeground,1542listFocusAndSelectionForeground: this.styles.listForeground,1543listActiveSelectionIconForeground: undefined,1544listInactiveSelectionIconForeground: undefined,1545}1546}));15471548this._register(this.tree.onDidChangeFocus(() => {1549const focused = this.tree.getFocus();1550if (focused && focused.length > 0) {1551const focusedItem = focused[0];1552const items = this.tree.getNode(null).children;1553const idx = items.findIndex(i => i.element === focusedItem);1554if (idx !== -1) {1555this._mostRecentlyFocusedItemIndex = idx;1556}1557}1558}));1559this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));15601561this._register(this.tree.onDidChangeContentHeight(() => {1562this.onDidChangeTreeContentHeight();1563}));1564this._register(this.renderer.onDidChangeItemHeight(e => {1565if (this.tree.hasElement(e.element) && this.visible) {1566this.tree.updateElementHeight(e.element, e.height);1567}1568}));1569this._register(this.tree.onDidFocus(() => {1570this._onDidFocus.fire();1571}));1572this._register(this.tree.onDidScroll(() => {1573this._onDidScroll.fire();15741575const isScrolledDown = this.tree.scrollTop >= this.tree.scrollHeight - this.tree.renderHeight - 2;1576this.container.classList.toggle('show-scroll-down', !isScrolledDown && !this.scrollLock);1577}));1578}15791580startEditing(requestId: string): void {1581const editedRequest = this.renderer.getTemplateDataForRequestId(requestId);1582if (editedRequest) {1583this.clickedRequest(editedRequest);1584}1585}15861587private clickedRequest(item: IChatListItemTemplate) {15881589const currentElement = item.currentElement;1590if (isRequestVM(currentElement) && !this.viewModel?.editing) {15911592const requests = this.viewModel?.model.getRequests();1593if (!requests || !this.viewModel?.sessionResource) {1594return;1595}15961597// this will only ever be true if we restored a checkpoint1598if (this.viewModel?.model.checkpoint) {1599this.recentlyRestoredCheckpoint = true;1600}16011602this.viewModel?.model.setCheckpoint(currentElement.id);16031604// set contexts and request to false1605const currentContext: IChatRequestVariableEntry[] = [];1606const addedContextIds = new Set<string>();1607const addToContext = (entry: IChatRequestVariableEntry) => {1608if (addedContextIds.has(entry.id) || isWorkspaceVariableEntry(entry)) {1609return;1610}1611if ((isPromptFileVariableEntry(entry) || isPromptTextVariableEntry(entry)) && entry.automaticallyAdded) {1612return;1613}1614addedContextIds.add(entry.id);1615currentContext.push(entry);1616};1617for (let i = requests.length - 1; i >= 0; i -= 1) {1618const request = requests[i];1619if (request.id === currentElement.id) {1620request.setShouldBeBlocked(false); // unblocking just this request.1621request.attachedContext?.forEach(addToContext);1622currentElement.variables.forEach(addToContext);1623}1624}16251626// set states1627this.viewModel?.setEditing(currentElement);1628if (item?.contextKeyService) {1629ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true);1630}16311632const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';1633this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);16341635if (!isInput) {1636const rowContainer = item.rowContainer;1637this.inputContainer = dom.$('.chat-edit-input-container');1638rowContainer.appendChild(this.inputContainer);1639this.createInput(this.inputContainer);1640this.input.setChatMode(this.inputPart.currentModeObs.get().id);1641} else {1642this.inputPart.element.classList.add('editing');1643}16441645this.inputPart.toggleChatInputOverlay(!isInput);1646if (currentContext.length > 0) {1647this.input.attachmentModel.addContext(...currentContext);1648}164916501651// rerenders1652this.inputPart.dnd.setDisabledOverlay(!isInput);1653this.input.renderAttachedContext();1654this.input.setValue(currentElement.messageText, false);1655this.renderer.updateItemHeightOnRender(currentElement, item);1656this.onDidChangeItems();1657this.input.inputEditor.focus();16581659this._register(this.inputPart.onDidClickOverlay(() => {1660if (this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1661this.finishedEditing();1662}1663}));16641665// listeners1666if (!isInput) {1667this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => {1668this.scrollToCurrentItem(currentElement);1669}));16701671this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => {1672this.scrollToCurrentItem(currentElement);1673}));1674}1675}16761677type StartRequestEvent = { editRequestType: string };16781679type StartRequestEventClassification = {1680owner: 'justschen';1681comment: 'Event used to gain insights into when edits are being pressed.';1682editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1683};16841685this.telemetryService.publicLog2<StartRequestEvent, StartRequestEventClassification>('chat.startEditingRequests', {1686editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1687});1688}16891690finishedEditing(completedEdit?: boolean): void {1691// reset states1692const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1693if (this.recentlyRestoredCheckpoint) {1694this.recentlyRestoredCheckpoint = false;1695} else {1696this.viewModel?.model.setCheckpoint(undefined);1697}1698this.inputPart.dnd.setDisabledOverlay(false);1699if (editedRequest?.contextKeyService) {1700ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false);1701}17021703const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';17041705if (!isInput) {1706this.inputPart.setChatMode(this.input.currentModeObs.get().id);1707const currentModel = this.input.selectedLanguageModel;1708if (currentModel) {1709this.inputPart.switchModel(currentModel.metadata);1710}17111712this.inputPart?.toggleChatInputOverlay(false);1713try {1714if (editedRequest?.rowContainer?.contains(this.inputContainer)) {1715editedRequest.rowContainer.removeChild(this.inputContainer);1716} else if (this.inputContainer.parentElement) {1717this.inputContainer.parentElement.removeChild(this.inputContainer);1718}1719} catch (e) {1720this.logService.error('Error occurred while finishing editing:', e);1721}1722this.inputContainer = dom.$('.empty-chat-state');17231724// only dispose if we know the input is not the bottom input object.1725this.input.dispose();1726}17271728if (isInput) {1729this.inputPart.element.classList.remove('editing');1730}1731this.viewModel?.setEditing(undefined);17321733this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);17341735this.onDidChangeItems();1736if (editedRequest?.currentElement) {1737this.renderer.updateItemHeightOnRender(editedRequest.currentElement, editedRequest);1738}17391740type CancelRequestEditEvent = {1741editRequestType: string;1742editCanceled: boolean;1743};17441745type CancelRequestEventEditClassification = {1746owner: 'justschen';1747editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1748editCanceled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether the edit was canceled.' };1749comment: 'Event used to gain insights into when edits are being canceled.';1750};17511752this.telemetryService.publicLog2<CancelRequestEditEvent, CancelRequestEventEditClassification>('chat.editRequestsFinished', {1753editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1754editCanceled: !completedEdit1755});17561757this.inputPart.focus();1758}17591760private scrollToCurrentItem(currentElement: IChatRequestViewModel): void {1761if (this.viewModel?.editing && currentElement) {1762const element = currentElement;1763if (!this.tree.hasElement(element)) {1764return;1765}1766const relativeTop = this.tree.getRelativeTop(element);1767if (relativeTop === null || relativeTop < 0 || relativeTop > 1) {1768this.tree.reveal(element, 0);1769}1770}1771}17721773private onContextMenu(e: ITreeContextMenuEvent<ChatTreeItem | null>): void {1774e.browserEvent.preventDefault();1775e.browserEvent.stopPropagation();17761777const selected = e.element;17781779// Check if the context menu was opened on a KaTeX element1780const target = e.browserEvent.target as HTMLElement;1781const isKatexElement = target.closest(`.${katexContainerClassName}`) !== null;17821783const scopedContextKeyService = this.contextKeyService.createOverlay([1784[ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered],1785[ChatContextKeys.isKatexMathElement.key, isKatexElement]1786]);1787this.contextMenuService.showContextMenu({1788menuId: MenuId.ChatContext,1789menuActionOptions: { shouldForwardArgs: true },1790contextKeyService: scopedContextKeyService,1791getAnchor: () => e.anchor,1792getActionsContext: () => selected,1793});1794}17951796private onDidChangeTreeContentHeight(): void {1797// If the list was previously scrolled all the way down, ensure it stays scrolled down, if scroll lock is on1798if (this.tree.scrollHeight !== this.previousTreeScrollHeight) {1799const lastItem = this.viewModel?.getItems().at(-1);1800const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;1801if (!lastResponseIsRendering || this.scrollLock) {1802// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.1803// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.1804const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2;1805if (lastElementWasVisible) {1806this.scrollAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {1807// Can't set scrollTop during this event listener, the list might overwrite the change18081809this.scrollToEnd();1810}, 0);1811}1812}1813}18141815// TODO@roblourens add `show-scroll-down` class when button should show1816// Show the button when content height changes, the list is not fully scrolled down, and (the latest response is currently rendering OR I haven't yet scrolled all the way down since the last response)1817// So for example it would not reappear if I scroll up and delete a message18181819this.previousTreeScrollHeight = this.tree.scrollHeight;1820this._onDidChangeContentHeight.fire();1821}18221823private getWidgetViewKindTag(): string {1824if (!this.viewContext) {1825return 'editor';1826} else if (isIChatViewViewContext(this.viewContext)) {1827return 'view';1828} else {1829return 'quick';1830}1831}18321833private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'compact' | 'minimal'; renderInputToolbarBelowInput?: boolean }): void {1834const commonConfig: IChatInputPartOptions = {1835renderFollowups: options?.renderFollowups ?? true,1836renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle,1837renderInputToolbarBelowInput: options?.renderInputToolbarBelowInput ?? false,1838menus: {1839executeToolbar: MenuId.ChatExecute,1840telemetrySource: 'chatWidget',1841...this.viewOptions.menus1842},1843editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,1844enableImplicitContext: this.viewOptions.enableImplicitContext,1845renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit',1846supportsChangingModes: this.viewOptions.supportsChangingModes,1847dndContainer: this.viewOptions.dndContainer,1848widgetViewKindTag: this.getWidgetViewKindTag(),1849defaultMode: this.viewOptions.defaultMode1850};18511852if (this.viewModel?.editing) {1853const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1854const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService])));1855this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart,1856this.location,1857commonConfig,1858this.styles,1859true1860);1861} else {1862this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart,1863this.location,1864commonConfig,1865this.styles,1866false1867);1868}18691870this.input.render(container, '', this);18711872this._register(this.input.onDidLoadInputState(() => {1873this.refreshParsedInput();1874}));1875this._register(this.input.onDidFocus(() => this._onDidFocus.fire()));1876this._register(this.input.onDidAcceptFollowup(e => {1877if (!this.viewModel) {1878return;1879}18801881let msg = '';1882if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind)?.id) {1883const agent = this.chatAgentService.getAgent(e.followup.agentId);1884if (!agent) {1885return;1886}18871888this.lastSelectedAgent = agent;1889msg = `${chatAgentLeader}${agent.name} `;1890if (e.followup.subCommand) {1891msg += `${chatSubcommandLeader}${e.followup.subCommand} `;1892}1893} else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) {1894msg = `${chatSubcommandLeader}${e.followup.subCommand} `;1895}18961897msg += e.followup.message;1898this.acceptInput(msg);18991900if (!e.response) {1901// Followups can be shown by the welcome message, then there is no response associated.1902// At some point we probably want telemetry for these too.1903return;1904}19051906this.chatService.notifyUserAction({1907sessionResource: this.viewModel.sessionResource,1908requestId: e.response.requestId,1909agentId: e.response.agent?.id,1910command: e.response.slashCommand?.name,1911result: e.response.result,1912action: {1913kind: 'followUp',1914followup: e.followup1915},1916});1917}));1918this._register(this.input.onDidChangeHeight(() => {1919const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1920if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) {1921this.renderer.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest);1922}19231924if (this.bodyDimension) {1925this.layout(this.bodyDimension.height, this.bodyDimension.width);1926}19271928this._onDidChangeContentHeight.fire();1929}));1930this._register(this.inputEditor.onDidChangeModelContent(() => {1931this.parsedChatRequest = undefined;1932this.updateChatInputContext();1933}));1934this._register(this.chatAgentService.onDidChangeAgents(() => {1935this.parsedChatRequest = undefined;1936// Tools agent loads -> welcome content changes1937this.renderWelcomeViewContentIfNeeded();1938}));1939this._register(this.input.onDidChangeCurrentChatMode(() => {1940this.renderWelcomeViewContentIfNeeded();1941this.refreshParsedInput();1942this.renderFollowups();1943this.renderChatSuggestNextWidget();1944}));19451946this._register(autorun(r => {1947const toolSetIds = new Set<string>();1948const toolIds = new Set<string>();1949for (const [entry, enabled] of this.input.selectedToolsModel.entriesMap.read(r)) {1950if (enabled) {1951if (entry instanceof ToolSet) {1952toolSetIds.add(entry.id);1953} else {1954toolIds.add(entry.id);1955}1956}1957}1958const disabledTools = this.input.attachmentModel.attachments1959.filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id))1960.map(a => a.id);19611962this.input.attachmentModel.updateContext(disabledTools, Iterable.empty());1963this.refreshParsedInput();1964}));1965}19661967private onDidStyleChange(): void {1968this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? '');1969this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? '');1970this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? '');1971}197219731974setModel(model: IChatModel | undefined): void {1975if (!this.container) {1976throw new Error('Call render() before setModel()');1977}19781979if (!model) {1980if (this.viewModel?.editing) {1981this.finishedEditing();1982}1983this.viewModel = undefined;1984this.onDidChangeItems();1985return;1986}19871988if (isEqual(model.sessionResource, this.viewModel?.sessionResource)) {1989return;1990}19911992if (this.viewModel?.editing) {1993this.finishedEditing();1994}1995this.inputPart.clearTodoListWidget(model.sessionResource, false);1996this.chatSuggestNextWidget.hide();19971998this._codeBlockModelCollection.clear();19992000this.container.setAttribute('data-session-id', model.sessionId);2001this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection);20022003// Pass input model reference to input part for state syncing2004this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0);20052006if (this._lockedAgent) {2007let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id);2008if (!placeholder) {2009placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgent.id);2010}2011this.viewModel.setInputPlaceholder(placeholder);2012this.inputEditor.updateOptions({ placeholder });2013} else if (this.viewModel.inputPlaceholder) {2014this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });2015}20162017const renderImmediately = this.configurationService.getValue<boolean>('chat.experimental.renderMarkdownImmediately');2018const delay = renderImmediately ? MicrotaskDelay : 0;2019this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, delay), (events => {2020if (!this.viewModel || this._store.isDisposed) {2021// See https://github.com/microsoft/vscode/issues/2789692022return;2023}20242025this.requestInProgress.set(this.viewModel.model.requestInProgress.get());20262027// Update the editor's placeholder text when it changes in the view model2028if (events?.some(e => e?.kind === 'changePlaceholder')) {2029this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });2030}20312032this.onDidChangeItems();2033if (events?.some(e => e?.kind === 'addRequest') && this.visible) {2034this.scrollToEnd();2035}2036})));2037this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => {2038// Ensure that view state is saved here, because we will load it again when a new model is assigned2039if (this.viewModel?.editing) {2040this.finishedEditing();2041}2042// Disposes the viewmodel and listeners2043this.viewModel = undefined;2044this.onDidChangeItems();2045}));2046this._sessionIsEmptyContextKey.set(model.getRequests().length === 0);20472048this.refreshParsedInput();2049this.viewModelDisposables.add(model.onDidChange((e) => {2050if (e.kind === 'setAgent') {2051this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command });2052// Update capabilities context keys when agent changes2053this._updateAgentCapabilitiesContextKeys(e.agent);2054}2055if (e.kind === 'addRequest') {2056this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false);2057this._sessionIsEmptyContextKey.set(false);2058}2059// Hide widget on request removal2060if (e.kind === 'removeRequest') {2061this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);2062this.chatSuggestNextWidget.hide();2063this._sessionIsEmptyContextKey.set((this.viewModel?.model.getRequests().length ?? 0) === 0);2064}2065// Show next steps widget when response completes (not when request starts)2066if (e.kind === 'completedRequest') {2067const lastRequest = this.viewModel?.model.getRequests().at(-1);2068const wasCancelled = lastRequest?.response?.isCanceled ?? false;2069if (wasCancelled) {2070// Clear todo list when request is cancelled2071this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);2072}2073// Only show if response wasn't canceled2074this.renderChatSuggestNextWidget();2075}2076}));20772078if (this.tree && this.visible) {2079this.onDidChangeItems();2080this.scrollToEnd();2081}20822083this.renderer.updateViewModel(this.viewModel);2084this.updateChatInputContext();2085this.input.renderChatTodoListWidget(this.viewModel.sessionResource);2086}20872088getFocus(): ChatTreeItem | undefined {2089return this.tree.getFocus()[0] ?? undefined;2090}20912092reveal(item: ChatTreeItem, relativeTop?: number): void {2093this.tree.reveal(item, relativeTop);2094}20952096focus(item: ChatTreeItem): void {2097const items = this.tree.getNode(null).children;2098const node = items.find(i => i.element?.id === item.id);2099if (!node) {2100return;2101}21022103this._mostRecentlyFocusedItemIndex = items.indexOf(node);2104this.tree.setFocus([node.element]);2105this.tree.domFocus();2106}21072108setInputPlaceholder(placeholder: string): void {2109this.viewModel?.setInputPlaceholder(placeholder);2110}21112112resetInputPlaceholder(): void {2113this.viewModel?.resetInputPlaceholder();2114}21152116setInput(value = ''): void {2117this.input.setValue(value, false);2118this.refreshParsedInput();2119}21202121getInput(): string {2122return this.input.inputEditor.getValue();2123}21242125getContrib<T extends IChatWidgetContrib>(id: string): T | undefined {2126return this.contribs.find(c => c.id === id) as T | undefined;2127}21282129// Coding agent locking methods2130lockToCodingAgent(name: string, displayName: string, agentId: string): void {2131this._lockedAgent = {2132id: agentId,2133name,2134prefix: `@${name} `,2135displayName2136};2137this._lockedToCodingAgentContextKey.set(true);2138this.renderWelcomeViewContentIfNeeded();2139// Update capabilities for the locked agent2140const agent = this.chatAgentService.getAgent(agentId);2141this._updateAgentCapabilitiesContextKeys(agent);2142this.renderer.updateOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true });2143if (this.visible) {2144this.tree.rerender();2145}2146}21472148unlockFromCodingAgent(): void {2149// Clear all state related to locking2150this._lockedAgent = undefined;2151this._lockedToCodingAgentContextKey.set(false);2152this._updateAgentCapabilitiesContextKeys(undefined);21532154// Explicitly update the DOM to reflect unlocked state2155this.renderWelcomeViewContentIfNeeded();21562157// Reset to default placeholder2158if (this.viewModel) {2159this.viewModel.resetInputPlaceholder();2160}2161this.inputEditor.updateOptions({ placeholder: undefined });2162this.renderer.updateOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask });2163if (this.visible) {2164this.tree.rerender();2165}2166}21672168get isLockedToCodingAgent(): boolean {2169return !!this._lockedAgent;2170}21712172get lockedAgentId(): string | undefined {2173return this._lockedAgent?.id;2174}21752176logInputHistory(): void {2177this.input.logInputHistory();2178}21792180async acceptInput(query?: string, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {2181return this._acceptInput(query ? { query } : undefined, options);2182}21832184async rerunLastRequest(): Promise<void> {2185if (!this.viewModel) {2186return;2187}21882189const sessionResource = this.viewModel.sessionResource;2190const lastRequest = this.chatService.getSession(sessionResource)?.getRequests().at(-1);2191if (!lastRequest) {2192return;2193}21942195const options: IChatSendRequestOptions = {2196attempt: lastRequest.attempt + 1,2197location: this.location,2198userSelectedModelId: this.input.currentLanguageModel2199};2200return await this.chatService.resendRequest(lastRequest, options);2201}22022203private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise<void> {2204// first check if the input has a prompt slash command2205const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart);2206if (!agentSlashPromptPart) {2207return;2208}22092210// need to resolve the slash command to get the prompt file2211const slashCommand = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.name, CancellationToken.None);2212if (!slashCommand) {2213return;2214}2215const parseResult = slashCommand.parsedPromptFile;2216// add the prompt file to the context2217const refs = parseResult.body?.variableReferences.map(({ name, offset }) => ({ name, range: new OffsetRange(offset, offset + name.length + 1) })) ?? [];2218const toolReferences = this.toolsService.toToolReferences(refs);2219requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));22202221// remove the slash command from the input2222requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim();22232224const input = requestInput.input.trim();2225requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`;2226if (input) {2227// if the input is not empty, append it to the prompt2228requestInput.input += `\n${input}`;2229}2230if (parseResult.header) {2231await this._applyPromptMetadata(parseResult.header, requestInput);2232}2233}22342235private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {2236if (this.viewModel?.model.requestInProgress.get()) {2237return;2238}22392240if (!query && this.input.generating) {2241// if the user submits the input and generation finishes quickly, just submit it for them2242const generatingAutoSubmitWindow = 500;2243const start = Date.now();2244await this.input.generating;2245if (Date.now() - start > generatingAutoSubmitWindow) {2246return;2247}2248}22492250while (!this._viewModel && !this._store.isDisposed) {2251await Event.toPromise(this.onDidChangeViewModel, this._store);2252}22532254if (!this.viewModel) {2255return;2256}22572258this._onDidAcceptInput.fire();2259this.scrollLock = this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll);22602261const editorValue = this.getInput();2262const requestInputs: IChatRequestInputOptions = {2263input: !query ? editorValue : query.query,2264attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionResource) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionResource),2265};22662267const isUserQuery = !query;22682269if (this.viewModel?.editing) {2270this.finishedEditing(true);2271this.viewModel.model?.setCheckpoint(undefined);2272}22732274// process the prompt command2275await this._applyPromptFileIfSet(requestInputs);2276await this._autoAttachInstructions(requestInputs);22772278if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) {2279const 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 set2280const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext;22812282// Collect file variables from previous requests before sending the request2283const previousRequests = this.viewModel.model.getRequests();2284for (const request of previousRequests) {2285for (const variable of request.variableData.variables) {2286if (URI.isUri(variable.value) && variable.kind === 'file') {2287const uri = variable.value;2288if (!uniqueWorkingSetEntries.has(uri)) {2289editingSessionAttachedContext.add(variable);2290uniqueWorkingSetEntries.add(variable.value);2291}2292}2293}2294}2295requestInputs.attachedContext = editingSessionAttachedContext;22962297type ChatEditingWorkingSetClassification = {2298owner: 'joyceerhl';2299comment: 'Information about the working set size in a chat editing request';2300originalSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that the user tried to attach in their editing request.' };2301actualSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that were actually sent in their editing request.' };2302};2303type ChatEditingWorkingSetEvent = {2304originalSize: number;2305actualSize: number;2306};2307this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size });2308}2309this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource);2310if (this.currentRequest) {2311// We have to wait the current request to be properly cancelled so that it has a chance to update the model with its result metadata.2312// This is awkward, it's basically a limitation of the chat provider-based agent.2313await Promise.race([this.currentRequest, timeout(1000)]);2314}23152316this.input.validateAgentMode();23172318if (this.viewModel.model.checkpoint) {2319const requests = this.viewModel.model.getRequests();2320for (let i = requests.length - 1; i >= 0; i -= 1) {2321const request = requests[i];2322if (request.shouldBeBlocked) {2323this.chatService.removeRequest(this.viewModel.sessionResource, request.id);2324}2325}2326}2327if (this.viewModel.sessionResource) {2328this.chatAccessibilityService.acceptRequest(this._viewModel!.sessionResource);2329}23302331const result = await this.chatService.sendRequest(this.viewModel.sessionResource, requestInputs.input, {2332userSelectedModelId: this.input.currentLanguageModel,2333location: this.location,2334locationData: this._location.resolveData?.(),2335parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind },2336attachedContext: requestInputs.attachedContext.asArray(),2337noCommandDetection: options?.noCommandDetection,2338...this.getModeRequestOptions(),2339modeInfo: this.input.currentModeInfo,2340agentIdSilent: this._lockedAgent?.id,2341});23422343if (!result) {2344this.chatAccessibilityService.disposeRequest(this.viewModel.sessionResource);2345return;2346}23472348// visibility sync before we accept input to hide the welcome view2349this.updateChatViewVisibility();23502351this.input.acceptInput(options?.storeToHistory ?? isUserQuery);2352this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });2353this.handleDelegationExitIfNeeded(this._lockedAgent, result.agent);2354this.currentRequest = result.responseCompletePromise.then(() => {2355const responses = this.viewModel?.getItems().filter(isResponseVM);2356const lastResponse = responses?.[responses.length - 1];2357this.chatAccessibilityService.acceptResponse(this, this.container, lastResponse, this.viewModel?.sessionResource, options?.isVoiceInput);2358if (lastResponse?.result?.nextQuestion) {2359const { prompt, participant, command } = lastResponse.result.nextQuestion;2360const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command);2361if (question) {2362this.input.setValue(question, false);2363}2364}2365this.currentRequest = undefined;2366});23672368return result.responseCreatedPromise;2369}23702371getModeRequestOptions(): Partial<IChatSendRequestOptions> {2372return {2373modeInfo: this.input.currentModeInfo,2374userSelectedTools: this.input.selectedToolsModel.userSelectedTools,2375};2376}23772378getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] {2379return this.renderer.getCodeBlockInfosForResponse(response);2380}23812382getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined {2383return this.renderer.getCodeBlockInfoForEditor(uri);2384}23852386getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] {2387return this.renderer.getFileTreeInfosForResponse(response);2388}23892390getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined {2391return this.renderer.getLastFocusedFileTreeForResponse(response);2392}23932394focusResponseItem(lastFocused?: boolean): void {2395if (!this.viewModel) {2396return;2397}2398const items = this.tree.getNode(null).children;2399let item;2400if (lastFocused) {2401item = items[this._mostRecentlyFocusedItemIndex] ?? items[items.length - 1];2402} else {2403item = items[items.length - 1];2404}2405if (!item) {2406return;2407}24082409this.tree.setFocus([item.element]);2410this.tree.domFocus();2411}24122413layout(height: number, width: number): void {2414width = Math.min(width, this.viewOptions.renderStyle === 'minimal' ? width : 950); // no min width of inline chat24152416const heightUpdated = this.bodyDimension && this.bodyDimension.height !== height;2417this.bodyDimension = new dom.Dimension(width, height);24182419const layoutHeight = this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height;2420if (this.viewModel?.editing) {2421this.inlineInputPart?.layout(layoutHeight, width);2422}24232424this.inputPart.layout(layoutHeight, width);24252426const inputHeight = this.inputPart.inputPartHeight;2427const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;2428const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2;2429const lastItem = this.viewModel?.getItems().at(-1);24302431const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight);2432if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') {2433this.listContainer.style.removeProperty('--chat-current-response-min-height');2434} else {2435this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px');2436if (heightUpdated && lastItem && this.visible && this.tree.hasElement(lastItem)) {2437this.tree.updateElementHeight(lastItem, undefined);2438}2439}2440this.tree.layout(contentHeight, width);24412442this.welcomeMessageContainer.style.height = `${contentHeight}px`;24432444this.renderer.layout(width);24452446const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;2447if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) {2448this.scrollToEnd();2449}2450this.listContainer.style.height = `${contentHeight}px`;24512452this._onDidChangeHeight.fire(height);2453}24542455private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean };24562457// An alternative to layout, this allows you to specify the number of ChatTreeItems2458// you want to show, and the max height of the container. It will then layout the2459// tree to show that many items.2460// TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used2461setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2462this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2463this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode()));24642465const mutableDisposable = this._register(new MutableDisposable());2466this._register(this.tree.onDidScroll((e) => {2467// TODO@TylerLeonhardt this should probably just be disposed when this is disabled2468// and then set up again when it is enabled again2469if (!this._dynamicMessageLayoutData?.enabled) {2470return;2471}2472mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {2473if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) {2474return;2475}2476const renderHeight = e.height;2477const diff = e.scrollHeight - renderHeight - e.scrollTop;2478if (diff === 0) {2479return;2480}24812482const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight);2483const width = this.bodyDimension?.width ?? this.container.offsetWidth;2484this.input.layout(possibleMaxHeight, width);2485const inputPartHeight = this.input.inputPartHeight;2486const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;2487const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight);2488this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width);2489});2490}));2491}24922493updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2494this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2495let hasChanged = false;2496let height = this.bodyDimension!.height;2497let width = this.bodyDimension!.width;2498if (maxHeight < this.bodyDimension!.height) {2499height = maxHeight;2500hasChanged = true;2501}2502const containerWidth = this.container.offsetWidth;2503if (this.bodyDimension?.width !== containerWidth) {2504width = containerWidth;2505hasChanged = true;2506}2507if (hasChanged) {2508this.layout(height, width);2509}2510}25112512get isDynamicChatTreeItemLayoutEnabled(): boolean {2513return this._dynamicMessageLayoutData?.enabled ?? false;2514}25152516set isDynamicChatTreeItemLayoutEnabled(value: boolean) {2517if (!this._dynamicMessageLayoutData) {2518return;2519}2520this._dynamicMessageLayoutData.enabled = value;2521}25222523layoutDynamicChatTreeItemMode(): void {2524if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) {2525return;2526}25272528const width = this.bodyDimension?.width ?? this.container.offsetWidth;2529this.input.layout(this._dynamicMessageLayoutData.maxHeight, width);2530const inputHeight = this.input.inputPartHeight;2531const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;25322533const totalMessages = this.viewModel.getItems();2534// grab the last N messages2535const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages);25362537const needsRerender = messages.some(m => m.currentRenderedHeight === undefined);2538const listHeight = needsRerender2539? this._dynamicMessageLayoutData.maxHeight2540: messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0);25412542this.layout(2543Math.min(2544// we add an additional 18px in order to show that there is scrollable content2545inputHeight + chatSuggestNextWidgetHeight + listHeight + (totalMessages.length > 2 ? 18 : 0),2546this._dynamicMessageLayoutData.maxHeight2547),2548width2549);25502551if (needsRerender || !listHeight) {2552this.scrollToEnd();2553}2554}25552556saveState(): void {2557// no-op2558}25592560getViewState(): IChatModelInputState | undefined {2561return this.input.getCurrentInputState();2562}25632564private updateChatInputContext() {2565const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart);2566this.agentInInput.set(!!currentAgent);2567}25682569private async _switchToAgentByName(agentName: string): Promise<void> {2570const currentAgent = this.input.currentModeObs.get();25712572// switch to appropriate agent if needed2573if (agentName !== currentAgent.name.get()) {2574// Find the mode object to get its kind2575const agent = this.chatModeService.findModeByName(agentName);2576if (agent) {2577if (currentAgent.kind !== agent.kind) {2578const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentAgent.kind, agent.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model);2579if (!chatModeCheck) {2580return;2581}25822583if (chatModeCheck.needToClearSession) {2584await this.clear();2585}2586}2587this.input.setChatMode(agent.id);2588}2589}2590}25912592private async _applyPromptMetadata({ agent, tools, model }: PromptHeader, requestInput: IChatRequestInputOptions): Promise<void> {25932594if (tools !== undefined && !agent && this.input.currentModeKind !== ChatModeKind.Agent) {2595agent = ChatMode.Agent.name.get();2596}2597// switch to appropriate agent if needed2598if (agent) {2599this._switchToAgentByName(agent);2600}26012602// if not tools to enable are present, we are done2603if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) {2604const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools, Target.VSCode);2605this.input.selectedToolsModel.set(enablementMap, true);2606}26072608if (model !== undefined) {2609this.input.switchModelByQualifiedName(model);2610}2611}26122613/**2614* Adds additional instructions to the context2615* - instructions that have a 'applyTo' pattern that matches the current input2616* - instructions referenced in the copilot settings 'copilot-instructions'2617* - instructions referenced in an already included instruction file2618*/2619private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {2620this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`);2621const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.entriesMap.get() : undefined;26222623const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools);2624await computer.collect(attachedContext, CancellationToken.None);2625}26262627delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {2628this.tree.delegateScrollFromMouseWheelEvent(browserEvent);2629}2630}263126322633