Path: blob/main/src/vs/workbench/contrib/chat/browser/chatWidget.ts
3296 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 * as dom from '../../../../base/browser/dom.js';6import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js';7import { Button } from '../../../../base/browser/ui/button/button.js';8import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js';9import { disposableTimeout, timeout } from '../../../../base/common/async.js';10import { CancellationToken } from '../../../../base/common/cancellation.js';11import { Codicon } from '../../../../base/common/codicons.js';12import { toErrorMessage } from '../../../../base/common/errorMessage.js';13import { Emitter, Event } from '../../../../base/common/event.js';14import { FuzzyScore } from '../../../../base/common/filters.js';15import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';16import { Iterable } from '../../../../base/common/iterator.js';17import { combinedDisposable, Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';18import { KeyCode } from '../../../../base/common/keyCodes.js';19import { ResourceSet } from '../../../../base/common/map.js';20import { Schemas } from '../../../../base/common/network.js';21import { autorun, observableFromEvent, observableValue } from '../../../../base/common/observable.js';22import { basename, extUri, isEqual } from '../../../../base/common/resources.js';23import { MicrotaskDelay } from '../../../../base/common/symbols.js';24import { isDefined } from '../../../../base/common/types.js';25import { URI } from '../../../../base/common/uri.js';26import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';27import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';28import { localize } from '../../../../nls.js';29import { MenuId } from '../../../../platform/actions/common/actions.js';30import { fromNowByDay, fromNow } from '../../../../base/common/date.js';31import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';32import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';33import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';34import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js';35import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';36import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';37import { WorkbenchObjectTree, WorkbenchList } from '../../../../platform/list/browser/listService.js';38import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';39import { ILogService } from '../../../../platform/log/common/log.js';40import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js';41import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';42import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../platform/theme/common/colorRegistry.js';43import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';44import { IThemeService } from '../../../../platform/theme/common/themeService.js';45import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';46import { checkModeOption } from '../common/chat.js';47import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../common/chatAgents.js';48import { ChatContextKeys } from '../common/chatContextKeys.js';49import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../common/chatEditingService.js';50import { IChatLayoutService } from '../common/chatLayoutService.js';51import { IChatModel, IChatResponseModel } from '../common/chatModel.js';52import { IChatModeService } from '../common/chatModes.js';53import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../common/chatParserTypes.js';54import { ChatRequestParser } from '../common/chatRequestParser.js';55import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../common/chatService.js';56import { IChatSlashCommandService } from '../common/chatSlashCommands.js';57import { IChatTodoListService } from '../common/chatTodoListService.js';58import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../common/chatVariableEntries.js';59import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js';60import { IChatInputState } from '../common/chatWidgetHistoryService.js';61import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js';62import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../common/constants.js';63import { ILanguageModelToolsService, IToolData, ToolSet } from '../common/languageModelToolsService.js';64import { ComputeAutomaticInstructions } from '../common/promptSyntax/computeAutomaticInstructions.js';65import { PromptsConfig } from '../common/promptSyntax/config/config.js';66import { type TPromptMetadata } from '../common/promptSyntax/parsers/promptHeader/promptHeader.js';67import { PromptsType } from '../common/promptSyntax/promptTypes.js';68import { IPromptParserResult, IPromptsService } from '../common/promptSyntax/service/promptsService.js';69import { TodoListToolSettingId } from '../common/tools/manageTodoListTool.js';70import { handleModeSwitch } from './actions/chatActions.js';71import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions, ChatViewId } from './chat.js';72import { ChatAccessibilityProvider } from './chatAccessibilityProvider.js';73import { ChatAttachmentModel } from './chatAttachmentModel.js';74import { ChatTodoListWidget } from './chatContentParts/chatTodoListWidget.js';75import { ChatInputPart, IChatInputStyles } from './chatInputPart.js';76import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js';77import { ChatEditorOptions } from './chatOptions.js';78import './media/chat.css';79import './media/chatAgentHover.css';80import './media/chatViewWelcome.css';81import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from './viewsWelcome/chatViewWelcomeController.js';82import { ChatViewPane } from './chatViewPane.js';83import { IViewsService } from '../../../services/views/common/viewsService.js';84import { ICommandService } from '../../../../platform/commands/common/commands.js';85import { IHoverService } from '../../../../platform/hover/browser/hover.js';8687const $ = dom.$;8889export interface IChatViewState {90inputValue?: string;91inputState?: IChatInputState;92}9394export interface IChatWidgetStyles extends IChatInputStyles {95inputEditorBackground: string;96resultEditorBackground: string;97}9899export interface IChatWidgetContrib extends IDisposable {100readonly id: string;101102/**103* A piece of state which is related to the input editor of the chat widget104*/105getInputState?(): any;106107/**108* Called with the result of getInputState when navigating input history.109*/110setInputState?(s: any): void;111}112113interface IChatRequestInputOptions {114input: string;115attachedContext: ChatRequestVariableSet;116}117118export interface IChatWidgetLocationOptions {119location: ChatAgentLocation;120resolveData?(): IChatLocationData | undefined;121}122123export function isQuickChat(widget: IChatWidget): boolean {124return 'viewContext' in widget && 'isQuickChat' in widget.viewContext && Boolean(widget.viewContext.isQuickChat);125}126127export function isInlineChat(widget: IChatWidget): boolean {128return 'viewContext' in widget && 'isInlineChat' in widget.viewContext && Boolean(widget.viewContext.isInlineChat);129}130131interface IChatHistoryListItem {132readonly sessionId: string;133readonly title: string;134readonly lastMessageDate: number;135readonly isActive: boolean;136}137138class ChatHistoryListDelegate implements IListVirtualDelegate<IChatHistoryListItem> {139getHeight(element: IChatHistoryListItem): number {140return 22;141}142143getTemplateId(element: IChatHistoryListItem): string {144return 'chatHistoryItem';145}146}147148interface IChatHistoryTemplate {149container: HTMLElement;150title: HTMLElement;151date: HTMLElement;152disposables: DisposableStore;153}154155class ChatHistoryListRenderer implements IListRenderer<IChatHistoryListItem, IChatHistoryTemplate> {156readonly templateId = 'chatHistoryItem';157158constructor(159private readonly onDidClickItem: (item: IChatHistoryListItem) => void,160private readonly hoverService: IHoverService,161private readonly formatHistoryTimestamp: (timestamp: number, todayMidnightMs: number) => string,162private readonly todayMidnightMs: number163) { }164165renderTemplate(container: HTMLElement): IChatHistoryTemplate {166const disposables = new DisposableStore();167168container.classList.add('chat-welcome-history-item');169const title = dom.append(container, $('.chat-welcome-history-title'));170const date = dom.append(container, $('.chat-welcome-history-date'));171172container.tabIndex = 0;173container.setAttribute('role', 'button');174175return { container, title, date, disposables };176}177178renderElement(element: IChatHistoryListItem, index: number, templateData: IChatHistoryTemplate): void {179const { container, title, date, disposables } = templateData;180181// Clear previous disposables182disposables.clear();183184// Set content185title.textContent = element.title;186date.textContent = this.formatHistoryTimestamp(element.lastMessageDate, this.todayMidnightMs);187188// Set accessibility189container.setAttribute('aria-label', element.title);190191// Setup hover for full title192const titleHoverEl = dom.$('div.chat-history-item-hover');193titleHoverEl.textContent = element.title;194disposables.add(this.hoverService.setupDelayedHover(container, {195content: titleHoverEl,196appearance: { showPointer: false, compact: true }197}));198199// Setup click and keyboard handlers200disposables.add(dom.addDisposableListener(container, dom.EventType.CLICK, () => {201this.onDidClickItem(element);202}));203204disposables.add(dom.addStandardDisposableListener(container, dom.EventType.KEY_DOWN, e => {205if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) {206e.preventDefault();207e.stopPropagation();208this.onDidClickItem(element);209}210}));211}212213disposeTemplate(templateData: IChatHistoryTemplate): void {214templateData.disposables.dispose();215}216}217218export class ChatWidget extends Disposable implements IChatWidget {219public static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = [];220221private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());222public readonly onDidSubmitAgent = this._onDidSubmitAgent.event;223224private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());225readonly onDidChangeAgent = this._onDidChangeAgent.event;226227private _onDidFocus = this._register(new Emitter<void>());228readonly onDidFocus = this._onDidFocus.event;229230private _onDidChangeViewModel = this._register(new Emitter<void>());231readonly onDidChangeViewModel = this._onDidChangeViewModel.event;232233private _onDidScroll = this._register(new Emitter<void>());234readonly onDidScroll = this._onDidScroll.event;235236private _onDidClear = this._register(new Emitter<void>());237readonly onDidClear = this._onDidClear.event;238239private _onDidAcceptInput = this._register(new Emitter<void>());240readonly onDidAcceptInput = this._onDidAcceptInput.event;241242private _onDidHide = this._register(new Emitter<void>());243readonly onDidHide = this._onDidHide.event;244245private _onDidShow = this._register(new Emitter<void>());246readonly onDidShow = this._onDidShow.event;247248private _onDidChangeParsedInput = this._register(new Emitter<void>());249readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event;250251private readonly _onWillMaybeChangeHeight = new Emitter<void>();252readonly onWillMaybeChangeHeight: Event<void> = this._onWillMaybeChangeHeight.event;253254private _onDidChangeHeight = this._register(new Emitter<number>());255readonly onDidChangeHeight = this._onDidChangeHeight.event;256257private readonly _onDidChangeContentHeight = new Emitter<void>();258readonly onDidChangeContentHeight: Event<void> = this._onDidChangeContentHeight.event;259260private contribs: ReadonlyArray<IChatWidgetContrib> = [];261262private tree!: WorkbenchObjectTree<ChatTreeItem, FuzzyScore>;263private renderer!: ChatListItemRenderer;264private readonly _codeBlockModelCollection: CodeBlockModelCollection;265private lastItem: ChatTreeItem | undefined;266267private readonly inputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());268private readonly inlineInputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());269private inputContainer!: HTMLElement;270private focusedInputDOM!: HTMLElement;271private editorOptions!: ChatEditorOptions;272273private recentlyRestoredCheckpoint: boolean = false;274275private settingChangeCounter = 0;276277private listContainer!: HTMLElement;278private container!: HTMLElement;279get domNode() {280return this.container;281}282283private welcomeMessageContainer!: HTMLElement;284private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());285private readonly historyViewStore = this._register(new DisposableStore());286private readonly chatTodoListWidget: ChatTodoListWidget;287private historyList: WorkbenchList<IChatHistoryListItem> | undefined;288289private bodyDimension: dom.Dimension | undefined;290private visibleChangeCount = 0;291private requestInProgress: IContextKey<boolean>;292private agentInInput: IContextKey<boolean>;293private inEmptyStateWithHistoryEnabledKey: IContextKey<boolean>;294private currentRequest: Promise<void> | undefined;295296private _visible = false;297public get visible() {298return this._visible;299}300301private previousTreeScrollHeight: number = 0;302303/**304* Whether the list is scroll-locked to the bottom. Initialize to true so that we can scroll to the bottom on first render.305* The initial render leads to a lot of `onDidChangeTreeContentHeight` as the renderer works out the real heights of rows.306*/307private scrollLock = true;308309private _isReady = false;310private _onDidBecomeReady = this._register(new Emitter<void>());311312private readonly viewModelDisposables = this._register(new DisposableStore());313private _viewModel: ChatViewModel | undefined;314315// Coding agent locking state316private _lockedToCodingAgent: string | undefined;317private _lockedToCodingAgentContextKey!: IContextKey<boolean>;318private _codingAgentPrefix: string | undefined;319private _lockedAgentId: string | undefined;320321private lastWelcomeViewChatMode: ChatModeKind | undefined;322323// Cache for prompt file descriptions to avoid async calls during rendering324private readonly promptDescriptionsCache = new Map<string, string>();325326private set viewModel(viewModel: ChatViewModel | undefined) {327if (this._viewModel === viewModel) {328return;329}330331this.viewModelDisposables.clear();332333this._viewModel = viewModel;334if (viewModel) {335this.viewModelDisposables.add(viewModel);336this.logService.debug('ChatWidget#setViewModel: have viewModel');337338if (viewModel.model.editingSessionObs) {339this.logService.debug('ChatWidget#setViewModel: waiting for editing session');340viewModel.model.editingSessionObs?.promise.then(() => {341this._isReady = true;342this._onDidBecomeReady.fire();343});344} else {345this._isReady = true;346this._onDidBecomeReady.fire();347}348} else {349this.logService.debug('ChatWidget#setViewModel: no viewModel');350}351352this._onDidChangeViewModel.fire();353}354355get viewModel() {356return this._viewModel;357}358359private readonly _editingSession = observableValue<IChatEditingSession | undefined>(this, undefined);360361private parsedChatRequest: IParsedChatRequest | undefined;362get parsedInput() {363if (this.parsedChatRequest === undefined) {364if (!this.viewModel) {365return { text: '', parts: [] };366}367368this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind });369this._onDidChangeParsedInput.fire();370}371372return this.parsedChatRequest;373}374375get scopedContextKeyService(): IContextKeyService {376return this.contextKeyService;377}378379private readonly _location: IChatWidgetLocationOptions;380get location() {381return this._location.location;382}383384readonly viewContext: IChatWidgetViewContext;385386private readonly chatSetupTriggerContext = ContextKeyExpr.or(387ChatContextKeys.Setup.installed.negate(),388ChatContextKeys.Entitlement.canSignUp389);390391get supportsChangingModes(): boolean {392return !!this.viewOptions.supportsChangingModes;393}394395get chatDisclaimer(): string {396return localize('chatDisclaimer', "AI responses may be inaccurate.");397}398399constructor(400location: ChatAgentLocation | IChatWidgetLocationOptions,401_viewContext: IChatWidgetViewContext | undefined,402private readonly viewOptions: IChatWidgetViewOptions,403private readonly styles: IChatWidgetStyles,404@ICodeEditorService private readonly codeEditorService: ICodeEditorService,405@IConfigurationService private readonly configurationService: IConfigurationService,406@IContextKeyService private readonly contextKeyService: IContextKeyService,407@IInstantiationService private readonly instantiationService: IInstantiationService,408@IChatService private readonly chatService: IChatService,409@IChatAgentService private readonly chatAgentService: IChatAgentService,410@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,411@IContextMenuService private readonly contextMenuService: IContextMenuService,412@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService,413@ILogService private readonly logService: ILogService,414@IThemeService private readonly themeService: IThemeService,415@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,416@IChatEditingService chatEditingService: IChatEditingService,417@ITelemetryService private readonly telemetryService: ITelemetryService,418@IPromptsService private readonly promptsService: IPromptsService,419@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,420@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,421@IChatModeService private readonly chatModeService: IChatModeService,422@IHoverService private readonly hoverService: IHoverService,423@IChatTodoListService private readonly chatTodoListService: IChatTodoListService,424@IChatLayoutService private readonly chatLayoutService: IChatLayoutService425) {426super();427this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);428429this.viewContext = _viewContext ?? {};430431const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel);432433if (typeof location === 'object') {434this._location = location;435} else {436this._location = { location };437}438439ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true);440ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location);441ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this));442this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService);443this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService);444445// Context key for when empty state history is enabled and in empty state446this.inEmptyStateWithHistoryEnabledKey = ChatContextKeys.inEmptyStateWithHistoryEnabled.bindTo(contextKeyService);447this._register(this.configurationService.onDidChangeConfiguration(e => {448if (e.affectsConfiguration(ChatConfiguration.EmptyStateHistoryEnabled)) {449this.updateEmptyStateWithHistoryContext();450this.renderWelcomeViewContentIfNeeded();451}452}));453this.updateEmptyStateWithHistoryContext();454455this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => {456const currentSession = this._editingSession.read(reader);457if (!currentSession) {458return;459}460const entries = currentSession.entries.read(reader);461const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified);462return decidedEntries.map(entry => entry.entryId);463}));464this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => {465const currentSession = this._editingSession.read(reader);466const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here467const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified);468return decidedEntries.length > 0;469}));470this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => {471const currentSession = this._editingSession.read(reader);472if (!currentSession) {473return false;474}475const entries = currentSession.entries.read(reader);476return entries.length > 0;477}));478this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => {479return this._editingSession.read(reader) !== null;480}));481this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => {482return this._editingSession.read(r)?.canUndo.read(r) || false;483}));484this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => {485return this._editingSession.read(r)?.canRedo.read(r) || false;486}));487this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => {488const chatModel = viewModelObs.read(r)?.model;489const editingSession = this._editingSession.read(r);490if (!editingSession || !chatModel) {491return false;492}493const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r);494return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete;495}));496497this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection, undefined));498this.chatTodoListWidget = this._register(this.instantiationService.createInstance(ChatTodoListWidget));499500this._register(this.configurationService.onDidChangeConfiguration((e) => {501if (e.affectsConfiguration('chat.renderRelatedFiles')) {502this.renderChatEditingSessionState();503}504505if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) {506this.settingChangeCounter++;507this.onDidChangeItems();508}509}));510511this._register(autorun(r => {512513const viewModel = viewModelObs.read(r);514const sessions = chatEditingService.editingSessionsObs.read(r);515516const session = sessions.find(candidate => candidate.chatSessionId === viewModel?.sessionId);517this._editingSession.set(undefined, undefined);518this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc.519520if (!session) {521// none or for a different chat widget522return;523}524525const entries = session.entries.read(r);526for (const entry of entries) {527entry.state.read(r); // SIGNAL528}529530this._editingSession.set(session, undefined);531532r.store.add(session.onDidDispose(() => {533this._editingSession.set(undefined, undefined);534this.renderChatEditingSessionState();535}));536r.store.add(this.onDidChangeParsedInput(() => {537this.renderChatEditingSessionState();538}));539r.store.add(this.inputEditor.onDidChangeModelContent(() => {540if (this.getInput() === '') {541this.refreshParsedInput();542this.renderChatEditingSessionState();543}544}));545this.renderChatEditingSessionState();546}));547548this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise<ICodeEditor | null> => {549const resource = input.resource;550if (resource.scheme !== Schemas.vscodeChatCodeBlock) {551return null;552}553554const responseId = resource.path.split('/').at(1);555if (!responseId) {556return null;557}558559const item = this.viewModel?.getItems().find(item => item.id === responseId);560if (!item) {561return null;562}563564// TODO: needs to reveal the chat view565566this.reveal(item);567568await timeout(0); // wait for list to actually render569570for (const codeBlockPart of this.renderer.editorsInUse()) {571if (extUri.isEqual(codeBlockPart.uri, resource, true)) {572const editor = codeBlockPart.editor;573574let relativeTop = 0;575const editorDomNode = editor.getDomNode();576if (editorDomNode) {577const row = dom.findParentWithClass(editorDomNode, 'monaco-list-row');578if (row) {579relativeTop = dom.getTopLeftOffset(editorDomNode).top - dom.getTopLeftOffset(row).top;580}581}582583if (input.options?.selection) {584const editorSelectionTopOffset = editor.getTopForPosition(input.options.selection.startLineNumber, input.options.selection.startColumn);585relativeTop += editorSelectionTopOffset;586587editor.focus();588editor.setSelection({589startLineNumber: input.options.selection.startLineNumber,590startColumn: input.options.selection.startColumn,591endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber,592endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn593});594}595596this.reveal(item, relativeTop);597598return editor;599}600}601return null;602}));603604this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext()));605606this._register(this.contextKeyService.onDidChangeContext(e => {607if (e.affectsSome(new Set([608ChatContextKeys.Setup.installed.key,609ChatContextKeys.Entitlement.canSignUp.key610]))) {611// reset the input in welcome view if it was rendered in experimental mode612if (this.container.classList.contains('experimental-welcome-view') && !this.contextKeyService.contextMatchesRules(this.chatSetupTriggerContext)) {613this.container.classList.remove('experimental-welcome-view');614const renderFollowups = this.viewOptions.renderFollowups ?? false;615const renderStyle = this.viewOptions.renderStyle;616this.createInput(this.container, { renderFollowups, renderStyle });617this.input.setChatMode(this.lastWelcomeViewChatMode ?? ChatModeKind.Ask);618}619}620}));621}622623private _lastSelectedAgent: IChatAgentData | undefined;624set lastSelectedAgent(agent: IChatAgentData | undefined) {625this.parsedChatRequest = undefined;626this._lastSelectedAgent = agent;627this._onDidChangeParsedInput.fire();628}629630get lastSelectedAgent(): IChatAgentData | undefined {631return this._lastSelectedAgent;632}633634get supportsFileReferences(): boolean {635return !!this.viewOptions.supportsFileReferences;636}637638get input(): ChatInputPart {639return this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart;640}641642private get inputPart(): ChatInputPart {643return this.inputPartDisposable.value!;644}645646private get inlineInputPart(): ChatInputPart {647return this.inlineInputPartDisposable.value!;648}649650get inputEditor(): ICodeEditor {651return this.input.inputEditor;652}653654get inputUri(): URI {655return this.input.inputUri;656}657658get contentHeight(): number {659return this.input.contentHeight + this.tree.contentHeight + this.chatTodoListWidget.height;660}661662get attachmentModel(): ChatAttachmentModel {663return this.input.attachmentModel;664}665666async waitForReady(): Promise<void> {667if (this._isReady) {668this.logService.debug('ChatWidget#waitForReady: already ready');669return;670}671672this.logService.debug('ChatWidget#waitForReady: waiting for ready');673await Event.toPromise(this._onDidBecomeReady.event);674675if (this.viewModel) {676this.logService.debug('ChatWidget#waitForReady: ready');677} else {678this.logService.debug('ChatWidget#waitForReady: no viewModel');679}680}681682render(parent: HTMLElement): void {683const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined;684this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground));685const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false;686const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop;687const renderStyle = this.viewOptions.renderStyle;688689this.container = dom.append(parent, $('.interactive-session'));690this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' }));691this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput()));692693dom.append(this.container, this.chatTodoListWidget.domNode);694this._register(this.chatTodoListWidget.onDidChangeHeight(() => {695if (this.bodyDimension) {696this.layout(this.bodyDimension.height, this.bodyDimension.width);697}698}));699700if (renderInputOnTop) {701this.createInput(this.container, { renderFollowups, renderStyle });702this.listContainer = dom.append(this.container, $(`.interactive-list`));703} else {704this.listContainer = dom.append(this.container, $(`.interactive-list`));705this.createInput(this.container, { renderFollowups, renderStyle });706}707708this.renderWelcomeViewContentIfNeeded();709this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle });710711const scrollDownButton = this._register(new Button(this.listContainer, {712supportIcons: true,713buttonBackground: asCssVariable(buttonSecondaryBackground),714buttonForeground: asCssVariable(buttonSecondaryForeground),715buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground),716}));717scrollDownButton.element.classList.add('chat-scroll-down');718scrollDownButton.label = `$(${Codicon.chevronDown.id})`;719scrollDownButton.setTitle(localize('scrollDownButtonLabel', "Scroll down"));720this._register(scrollDownButton.onDidClick(() => {721this.scrollLock = true;722this.scrollToEnd();723}));724725// Font size variables726this.container.style.setProperty('--vscode-chat-font-size-body-xs', '0.846em' /* 11px */);727this.container.style.setProperty('--vscode-chat-font-size-body-s', '0.923em' /* 12px */);728this.container.style.setProperty('--vscode-chat-font-size-body-m', '1em' /* 13px */);729this.container.style.setProperty('--vscode-chat-font-size-body-l', '1.077em' /* 14px */);730this.container.style.setProperty('--vscode-chat-font-size-body-xl', '1.231em' /* 16px */);731this.container.style.setProperty('--vscode-chat-font-size-body-xxl', '1.538em' /* 20px */);732733// Update the font family and size734this._register(autorun(reader => {735const fontFamily = this.chatLayoutService.fontFamily.read(reader);736const fontSize = this.chatLayoutService.fontSize.read(reader);737738this.container.style.setProperty('--vscode-chat-font-family', fontFamily);739this.container.style.fontSize = `${fontSize}px`;740741this.tree.rerender();742}));743744this._register(this.editorOptions.onDidChange(() => this.onDidStyleChange()));745this.onDidStyleChange();746747// Do initial render748if (this.viewModel) {749this.onDidChangeItems();750this.scrollToEnd();751}752753this.contribs = ChatWidget.CONTRIBS.map(contrib => {754try {755return this._register(this.instantiationService.createInstance(contrib, this));756} catch (err) {757this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err));758return undefined;759}760}).filter(isDefined);761762this._register((this.chatWidgetService as ChatWidgetService).register(this));763764const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput);765this._register(autorun(r => {766const input = parsedInput.read(r);767768const newPromptAttachments = new Map<string, IChatRequestVariableEntry>();769const oldPromptAttachments = new Set<string>();770771// get all attachments, know those that are prompt-referenced772for (const attachment of this.attachmentModel.attachments) {773if (attachment.range) {774oldPromptAttachments.add(attachment.id);775}776}777778// update/insert prompt-referenced attachments779for (const part of input.parts) {780if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) {781const entry = part.toVariableEntry();782newPromptAttachments.set(entry.id, entry);783oldPromptAttachments.delete(entry.id);784}785}786787this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values());788}));789790if (!this.focusedInputDOM) {791this.focusedInputDOM = this.container.appendChild(dom.$('.focused-input-dom'));792}793}794795private scrollToEnd() {796if (this.lastItem) {797const offset = Math.max(this.lastItem.currentRenderedHeight ?? 0, 1e6);798this.tree.reveal(this.lastItem, offset);799}800}801802getContrib<T extends IChatWidgetContrib>(id: string): T | undefined {803return this.contribs.find(c => c.id === id) as T;804}805806focusInput(): void {807this.input.focus();808809// Sometimes focusing the input part is not possible,810// but we'd like to be the last focused chat widget,811// so we emit an optimistic onDidFocus event nonetheless.812this._onDidFocus.fire();813}814815hasInputFocus(): boolean {816return this.input.hasFocus();817}818819refreshParsedInput() {820if (!this.viewModel) {821return;822}823this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind });824this._onDidChangeParsedInput.fire();825}826827getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined {828if (!isResponseVM(item)) {829return;830}831const items = this.viewModel?.getItems();832if (!items) {833return;834}835const responseItems = items.filter(i => isResponseVM(i));836const targetIndex = responseItems.indexOf(item);837if (targetIndex === undefined) {838return;839}840const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1;841if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) {842return;843}844return responseItems[indexToFocus];845}846847clear(): void {848this.logService.debug('ChatWidget#clear');849this._isReady = false;850if (this._dynamicMessageLayoutData) {851this._dynamicMessageLayoutData.enabled = true;852}853// Unlock coding agent when clearing854this.unlockFromCodingAgent();855this._onDidClear.fire();856}857858private onDidChangeItems(skipDynamicLayout?: boolean) {859// Update context key when items change860this.updateEmptyStateWithHistoryContext();861862if (this._visible || !this.viewModel) {863const treeItems = (this.viewModel?.getItems() ?? [])864.map((item): ITreeElement<ChatTreeItem> => {865return {866element: item,867collapsed: false,868collapsible: false869};870});871872873// reset the input in welcome view if it was rendered in experimental mode874if (this.container.classList.contains('experimental-welcome-view') && this.viewModel?.getItems().length) {875this.container.classList.remove('experimental-welcome-view');876const renderFollowups = this.viewOptions.renderFollowups ?? false;877const renderStyle = this.viewOptions.renderStyle;878this.createInput(this.container, { renderFollowups, renderStyle });879this.input.setChatMode(this.lastWelcomeViewChatMode ?? ChatModeKind.Ask);880}881882this.renderWelcomeViewContentIfNeeded();883this.renderChatTodoListWidget();884885this._onWillMaybeChangeHeight.fire();886887this.lastItem = treeItems.at(-1)?.element;888ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set(this.lastItem ? [this.lastItem.id] : []);889this.tree.setChildren(null, treeItems, {890diffIdentityProvider: {891getId: (element) => {892return element.dataId +893// Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied.894`${(isRequestVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` +895// If a response is in the process of progressive rendering, we need to ensure that it will896// be re-rendered so progressive rendering is restarted, even if the model wasn't updated.897`${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` +898// Re-render once content references are loaded899(isResponseVM(element) ? `_${element.contentReferences.length}` : '') +900// Re-render if element becomes hidden due to undo/redo901`_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` +902// Re-render if element becomes enabled/disabled due to checkpointing903`_${element.shouldBeBlocked ? '1' : '0'}` +904// Re-render if we have an element currently being edited905`_${this.viewModel?.editing ? '1' : '0'}` +906// Re-render if we have an element currently being checkpointed907`_${this.viewModel?.model.checkpoint ? '1' : '0'}` +908// Re-render all if invoked by setting change909`_setting${this.settingChangeCounter || '0'}` +910// Rerender request if we got new content references in the response911// since this may change how we render the corresponding attachments in the request912(isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : '');913},914}915});916917if (!skipDynamicLayout && this._dynamicMessageLayoutData) {918this.layoutDynamicChatTreeItemMode();919}920921this.renderFollowups();922}923}924925private renderWelcomeViewContentIfNeeded() {926927if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') {928return;929}930931const numItems = this.viewModel?.getItems().length ?? 0;932if (!numItems) {933const expEmptyState = this.configurationService.getValue<boolean>('chat.emptyChatState.enabled');934935let welcomeContent: IChatViewWelcomeContent;936const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind);937let additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage;938if (!additionalMessage) {939const generateInstructionsCommand = 'workbench.action.chat.generateInstructions';940additionalMessage = new MarkdownString(localize(941'chatWidget.instructions',942"[Generate instructions]({0}) to onboard AI onto your codebase.",943`command:${generateInstructionsCommand}`944), { isTrusted: { enabledCommands: [generateInstructionsCommand] } });945}946if (this.contextKeyService.contextMatchesRules(this.chatSetupTriggerContext)) {947welcomeContent = this.getExpWelcomeViewContent();948this.container.classList.add('experimental-welcome-view');949}950else if (expEmptyState) {951welcomeContent = this.getWelcomeViewContent(additionalMessage, expEmptyState);952}953else {954const tips = this.input.currentModeKind === ChatModeKind.Ask955? new MarkdownString(localize('chatWidget.tips', "{0} or type {1} to attach context\n\n{2} to chat with extensions\n\nType {3} to use commands", '$(attach)', '#', '$(mention)', '/'), { supportThemeIcons: true })956: new MarkdownString(localize('chatWidget.tips.withoutParticipants', "{0} or type {1} to attach context", '$(attach)', '#'), { supportThemeIcons: true });957welcomeContent = this.getWelcomeViewContent(additionalMessage);958welcomeContent.tips = tips;959}960if (!this.welcomePart.value || this.welcomePart.value.needsRerender(welcomeContent)) {961this.historyViewStore.clear();962dom.clearNode(this.welcomeMessageContainer);963964// Optional: recent chat history above welcome content when enabled965const showHistory = this.configurationService.getValue<boolean>(ChatConfiguration.EmptyStateHistoryEnabled);966if (showHistory && !this._lockedToCodingAgent) {967this.renderWelcomeHistorySection();968}969970this.welcomePart.value = this.instantiationService.createInstance(971ChatViewWelcomePart,972welcomeContent,973{974location: this.location,975isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent976}977);978dom.append(this.welcomeMessageContainer, this.welcomePart.value.element);979}980}981982if (this.viewModel) {983dom.setVisibility(numItems === 0, this.welcomeMessageContainer);984dom.setVisibility(numItems !== 0, this.listContainer);985}986}987988private updateEmptyStateWithHistoryContext(): void {989const historyEnabled = this.configurationService.getValue<boolean>(ChatConfiguration.EmptyStateHistoryEnabled);990const numItems = this.viewModel?.getItems().length ?? 0;991const shouldHideButtons = historyEnabled && numItems === 0;992this.inEmptyStateWithHistoryEnabledKey.set(shouldHideButtons);993}994995private async renderWelcomeHistorySection(): Promise<void> {996try {997const historyRoot = dom.append(this.welcomeMessageContainer, $('.chat-welcome-history-root'));998const container = dom.append(historyRoot, $('.chat-welcome-history'));999const header = dom.append(container, $('.chat-welcome-history-header'));1000const headerTitle = dom.append(header, $('.chat-welcome-history-header-title'));1001headerTitle.textContent = localize('chat.history.title', 'History');1002const headerActions = dom.append(header, $('.chat-welcome-history-header-actions'));10031004const items = await this.chatService.getHistory();1005const filtered = items1006.filter(i => !i.isActive)1007.sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0))1008.slice(0, 3);10091010// If no items to show, hide the entire chat history section1011if (filtered.length === 0) {1012historyRoot.remove();1013return;1014}10151016const showAllButton = dom.append(headerActions, $('.chat-welcome-history-show-all'));1017showAllButton.classList.add('codicon', `codicon-${Codicon.history.id}`, 'chat-welcome-history-show-all');1018showAllButton.tabIndex = 0;1019showAllButton.setAttribute('role', 'button');1020const showAllHover = localize('chat.history.showAllHover', 'Show history...');1021showAllButton.setAttribute('aria-label', showAllHover);1022const showAllHoverText = dom.$('div.chat-history-button-hover');1023showAllHoverText.textContent = showAllHover;10241025this.historyViewStore.add(this.hoverService.setupDelayedHover(showAllButton, { content: showAllHoverText, appearance: { showPointer: false, compact: true } }));10261027this.historyViewStore.add(dom.addDisposableListener(showAllButton, dom.EventType.CLICK, e => {1028e.preventDefault();1029e.stopPropagation();1030setTimeout(() => {1031this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('workbench.action.chat.history'));1032}, 0);1033}));10341035this.historyViewStore.add(dom.addStandardDisposableListener(showAllButton, dom.EventType.KEY_DOWN, e => {1036if (e.equals(KeyCode.Enter) || e.equals(KeyCode.Space)) {1037e.preventDefault();1038e.stopPropagation();1039setTimeout(() => {1040this.instantiationService.invokeFunction(accessor => accessor.get(ICommandService).executeCommand('workbench.action.chat.history'));1041}, 0);1042}1043}));1044const welcomeHistoryContainer = dom.append(container, $('.chat-welcome-history-list'));10451046this.welcomeMessageContainer.classList.toggle('has-chat-history', filtered.length > 0);10471048// Compute today's midnight once for label decisions1049const todayMidnight = new Date();1050todayMidnight.setHours(0, 0, 0, 0);1051const todayMidnightMs = todayMidnight.getTime();10521053// Create WorkbenchList for chat history items (limit to top 3)1054const historyItems: IChatHistoryListItem[] = filtered.slice(0, 3).map(item => ({1055sessionId: item.sessionId,1056title: item.title,1057lastMessageDate: typeof item.lastMessageDate === 'number' ? item.lastMessageDate : Date.now(),1058isActive: item.isActive1059}));10601061const listHeight = historyItems.length * 22;1062welcomeHistoryContainer.style.height = `${listHeight}px`;1063welcomeHistoryContainer.style.minHeight = `${listHeight}px`;1064welcomeHistoryContainer.style.overflow = 'hidden';10651066if (!this.historyList) {1067const delegate = new ChatHistoryListDelegate();1068const renderer = new ChatHistoryListRenderer(1069async (item) => await this.openHistorySession(item.sessionId),1070this.hoverService,1071(timestamp, todayMs) => this.formatHistoryTimestamp(timestamp, todayMs),1072todayMidnightMs1073);1074const list = this.instantiationService.createInstance(1075WorkbenchList<IChatHistoryListItem>,1076'ChatHistoryList',1077welcomeHistoryContainer,1078delegate,1079[renderer],1080{1081horizontalScrolling: false,1082keyboardSupport: true,1083mouseSupport: true,1084multipleSelectionSupport: false,1085overrideStyles: {1086listBackground: this.styles.listBackground1087},1088accessibilityProvider: {1089getAriaLabel: (item: IChatHistoryListItem) => item.title,1090getWidgetAriaLabel: () => localize('chat.history.list', 'Chat History')1091}1092}1093);1094this.historyList = this._register(list);1095} else {1096const currentHistoryList = this.historyList.getHTMLElement();1097if (currentHistoryList && currentHistoryList.parentElement !== welcomeHistoryContainer) {1098welcomeHistoryContainer.appendChild(currentHistoryList);1099}1100}11011102this.historyList.splice(0, this.historyList.length, historyItems);1103this.historyList.layout(undefined, listHeight);11041105// Deprecated text link replaced by icon button in header1106} catch (err) {1107this.logService.error('Failed to render welcome history', err);1108}1109}11101111private formatHistoryTimestamp(last: number, todayMidnightMs: number): string {1112if (last > todayMidnightMs) {1113const diffMs = Date.now() - last;1114const minMs = 60 * 1000;1115const adjusted = diffMs < minMs ? Date.now() - minMs : last;1116return fromNow(adjusted, true, true);1117}1118return fromNowByDay(last, true, true);1119}11201121private async openHistorySession(sessionId: string): Promise<void> {1122try {1123const viewsService = this.instantiationService.invokeFunction(accessor => accessor.get(IViewsService));1124const chatView = await viewsService.openView<ChatViewPane>(ChatViewId);1125await chatView?.loadSession?.(sessionId);1126} catch (e) {1127this.logService.error('Failed to open chat session from history', e);1128}1129}11301131private renderChatTodoListWidget(): void {1132const sessionId = this.viewModel?.sessionId;1133if (!sessionId) {1134this.chatTodoListWidget.render(sessionId);1135return;1136}11371138const isChatTodoListToolEnabled = this.configurationService.getValue<boolean>(TodoListToolSettingId) === true;1139if (!isChatTodoListToolEnabled) {1140return;1141}11421143const todos = this.chatTodoListService.getTodos(sessionId);1144if (todos.length > 0) {1145this.chatTodoListWidget.render(sessionId);1146}1147}11481149private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined, expEmptyState?: boolean): IChatViewWelcomeContent {1150const disclaimerMessage = expEmptyState1151? this.chatDisclaimer1152: localize('chatMessage', "Chat is powered by AI, so mistakes are possible. Review output carefully before use.");1153const icon = expEmptyState ? Codicon.chatSparkle : Codicon.copilotLarge;115411551156if (this.isLockedToCodingAgent) {1157// TODO(jospicer): Let extensions contribute this welcome message/docs1158const message = this._codingAgentPrefix === '@copilot '1159? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._codingAgentPrefix, 'https://aka.ms/coding-agent-docs') + this.chatDisclaimer, { isTrusted: true })1160: new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._codingAgentPrefix) + this.chatDisclaimer);11611162return {1163title: localize('codingAgentTitle', "Delegate to {0}", this._codingAgentPrefix),1164message,1165icon: Codicon.sendToRemoteAgent,1166additionalMessage,1167};1168}11691170const suggestedPrompts = this.getPromptFileSuggestions();11711172if (this.input.currentModeKind === ChatModeKind.Ask) {1173return {1174title: localize('chatDescription', "Ask about your code."),1175message: new MarkdownString(disclaimerMessage),1176icon,1177additionalMessage,1178suggestedPrompts1179};1180} else if (this.input.currentModeKind === ChatModeKind.Edit) {1181const editsHelpMessage = localize('editsHelp', "Start your editing session by defining a set of files that you want to work with. Then ask for the changes you want to make.");1182const message = expEmptyState ? disclaimerMessage : `${editsHelpMessage}\n\n${disclaimerMessage}`;11831184return {1185title: localize('editsTitle', "Edit in context."),1186message: new MarkdownString(message),1187icon,1188additionalMessage,1189suggestedPrompts1190};1191} else {1192const agentHelpMessage = localize('agentMessage', "Ask to edit your files in [agent mode]({0}). Agent mode will automatically use multiple requests to pick files to edit, run terminal commands, and iterate on errors.", 'https://aka.ms/vscode-copilot-agent');1193const message = expEmptyState ? disclaimerMessage : `${agentHelpMessage}\n\n${disclaimerMessage}`;11941195return {1196title: localize('agentTitle', "Build with agent mode."),1197message: new MarkdownString(message),1198icon,1199additionalMessage,1200suggestedPrompts1201};1202}1203}12041205private getExpWelcomeViewContent(): IChatViewWelcomeContent {1206const welcomeContent: IChatViewWelcomeContent = {1207title: localize('expChatTitle', 'Welcome to Copilot'),1208message: new MarkdownString(localize('expchatMessage', "Let's get started")),1209icon: Codicon.copilotLarge,1210inputPart: this.inputPart.element,1211additionalMessage: localize('expChatAdditionalMessage', "Review AI output carefully before use."),1212isExperimental: true,1213suggestedPrompts: this.getExpSuggestedPrompts(),1214};1215return welcomeContent;1216}12171218private getExpSuggestedPrompts(): IChatSuggestedPrompts[] {1219// Check if the workbench is empty1220const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY;1221if (isEmpty) {1222return [1223{1224icon: Codicon.vscode,1225label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"),1226prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"),1227},1228{1229icon: Codicon.newFolder,1230label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"),1231prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"),1232}1233];1234} else {1235return [1236{1237icon: Codicon.debugAlt,1238label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"),1239prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"),1240},1241{1242icon: Codicon.gear,1243label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"),1244prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"),1245}1246];1247}1248}12491250private getPromptFileSuggestions(): IChatSuggestedPrompts[] {1251// Get the prompt file suggestions configuration1252const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService);1253if (!suggestions) {1254return [];1255}12561257const result: IChatSuggestedPrompts[] = [];1258const promptsToLoad: string[] = [];12591260// First, collect all prompts that need loading (regardless of shouldInclude)1261for (const [promptName] of Object.entries(suggestions)) {1262const description = this.promptDescriptionsCache.get(promptName);1263if (description === undefined) {1264promptsToLoad.push(promptName);1265}1266}12671268// If we have prompts to load, load them asynchronously and don't return anything yet1269if (promptsToLoad.length > 0) {1270this.loadPromptDescriptions(promptsToLoad);1271return [];1272}12731274// Now process the suggestions with loaded descriptions1275const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = [];12761277for (const [promptName, condition] of Object.entries(suggestions)) {1278let score = 0;12791280// Handle boolean conditions1281if (typeof condition === 'boolean') {1282score = condition ? 1 : 0;1283}1284// Handle when clause conditions1285else if (typeof condition === 'string') {1286try {1287const whenClause = ContextKeyExpr.deserialize(condition);1288if (whenClause) {1289// Test against all open code editors1290const allEditors = this.codeEditorService.listCodeEditors();12911292if (allEditors.length > 0) {1293// Count how many editors match the when clause1294score = allEditors.reduce((count, editor) => {1295try {1296const editorContext = this.contextKeyService.getContext(editor.getDomNode());1297return count + (whenClause.evaluate(editorContext) ? 1 : 0);1298} catch (error) {1299// Log error for this specific editor but continue with others1300this.logService.warn('Failed to evaluate when clause for editor:', error);1301return count;1302}1303}, 0);1304} else {1305// Fallback to global context if no editors are open1306score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0;1307}1308} else {1309score = 0;1310}1311} catch (error) {1312// Log the error but don't fail completely1313this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error);1314score = 0;1315}1316}13171318if (score > 0) {1319promptsWithScores.push({ promptName, condition, score });1320}1321}13221323// Sort by score (descending) and take top 51324promptsWithScores.sort((a, b) => b.score - a.score);1325const topPrompts = promptsWithScores.slice(0, 5);13261327// Build the final result array1328for (const { promptName } of topPrompts) {1329const description = this.promptDescriptionsCache.get(promptName);1330result.push({1331icon: Codicon.run,1332label: description || localize('chatWidget.promptFile.suggestion', "/{0}", promptName),1333prompt: `/${promptName} `1334});1335}13361337return result;1338}13391340private async loadPromptDescriptions(promptNames: string[]): Promise<void> {1341try {1342// Get all available prompt files with their metadata1343const promptCommands = await this.promptsService.findPromptSlashCommands();13441345// Load descriptions only for the specified prompts1346for (const promptCommand of promptCommands) {1347if (promptNames.includes(promptCommand.command)) {1348try {1349if (promptCommand.promptPath) {1350const parseResult = await this.promptsService.parse(1351promptCommand.promptPath.uri,1352promptCommand.promptPath.type,1353CancellationToken.None1354);1355if (parseResult.metadata?.description) {1356this.promptDescriptionsCache.set(promptCommand.command, parseResult.metadata.description);1357} else {1358// Set empty string to indicate we've checked this prompt1359this.promptDescriptionsCache.set(promptCommand.command, '');1360}1361}1362} catch (error) {1363// Log the error but continue with other prompts1364this.logService.warn('Failed to parse prompt file for description:', promptCommand.command, error);1365// Set empty string to indicate we've checked this prompt1366this.promptDescriptionsCache.set(promptCommand.command, '');1367}1368}1369}13701371// Trigger a re-render of the welcome view to show the loaded descriptions1372this.renderWelcomeViewContentIfNeeded();1373} catch (error) {1374this.logService.warn('Failed to load specific prompt descriptions:', error);1375}1376}13771378private async renderChatEditingSessionState() {1379if (!this.input) {1380return;1381}1382this.input.renderChatEditingSessionState(this._editingSession.get() ?? null);13831384if (this.bodyDimension) {1385this.layout(this.bodyDimension.height, this.bodyDimension.width);1386}1387}13881389private async renderFollowups(): Promise<void> {1390if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete && this.input.currentModeKind === ChatModeKind.Ask) {1391this.input.renderFollowups(this.lastItem.replyFollowups, this.lastItem);1392} else {1393this.input.renderFollowups(undefined, undefined);1394}13951396if (this.bodyDimension) {1397this.layout(this.bodyDimension.height, this.bodyDimension.width);1398}1399}14001401setVisible(visible: boolean): void {1402const wasVisible = this._visible;1403this._visible = visible;1404this.visibleChangeCount++;1405this.renderer.setVisible(visible);1406this.input.setVisible(visible);14071408if (visible) {1409this._register(disposableTimeout(() => {1410// Progressive rendering paused while hidden, so start it up again.1411// Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here)1412if (this._visible) {1413this.onDidChangeItems(true);1414}1415}, 0));14161417if (!wasVisible) {1418dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {1419this._onDidShow.fire();1420});1421}1422} else if (wasVisible) {1423this._onDidHide.fire();1424}1425}14261427private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void {1428const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])));1429const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200);1430const rendererDelegate: IChatRendererDelegate = {1431getListLength: () => this.tree.getNode(null).visibleChildrenCount,1432onDidScroll: this.onDidScroll,1433container: listContainer,1434currentChatMode: () => this.input.currentModeKind,1435};14361437// Create a dom element to hold UI from editor widgets embedded in chat messages1438const overflowWidgetsContainer = document.createElement('div');1439overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor');1440listContainer.append(overflowWidgetsContainer);14411442this.renderer = this._register(scopedInstantiationService.createInstance(1443ChatListItemRenderer,1444this.editorOptions,1445options,1446rendererDelegate,1447this._codeBlockModelCollection,1448overflowWidgetsContainer,1449this.viewModel,1450));14511452this._register(this.renderer.onDidClickRequest(async item => {1453this.clickedRequest(item);1454}));14551456this._register(this.renderer.onDidRerender(item => {1457if (isRequestVM(item.currentElement) && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1458if (!item.rowContainer.contains(this.inputContainer)) {1459item.rowContainer.appendChild(this.inputContainer);1460}1461this.input.focus();1462}1463}));14641465this._register(this.renderer.onDidDispose((item) => {1466this.focusedInputDOM.appendChild(this.inputContainer);1467this.input.focus();1468}));14691470this._register(this.renderer.onDidFocusOutside(() => {1471this.finishedEditing();1472}));14731474this._register(this.renderer.onDidClickFollowup(item => {1475// is this used anymore?1476this.acceptInput(item.message);1477}));1478this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => {1479const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId);1480if (request) {1481const options: IChatSendRequestOptions = {1482noCommandDetection: true,1483attempt: request.attempt + 1,1484location: this.location,1485userSelectedModelId: this.input.currentLanguageModel,1486modeInfo: this.input.currentModeInfo,1487};1488this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e));1489}1490}));14911492this.tree = this._register(scopedInstantiationService.createInstance(1493WorkbenchObjectTree<ChatTreeItem, FuzzyScore>,1494'Chat',1495listContainer,1496delegate,1497[this.renderer],1498{1499identityProvider: { getId: (e: ChatTreeItem) => e.id },1500horizontalScrolling: false,1501alwaysConsumeMouseWheel: false,1502supportDynamicHeights: true,1503hideTwistiesOfChildlessElements: true,1504accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider),1505keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO1506setRowLineHeight: false,1507filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined,1508scrollToActiveElement: true,1509overrideStyles: {1510listFocusBackground: this.styles.listBackground,1511listInactiveFocusBackground: this.styles.listBackground,1512listActiveSelectionBackground: this.styles.listBackground,1513listFocusAndSelectionBackground: this.styles.listBackground,1514listInactiveSelectionBackground: this.styles.listBackground,1515listHoverBackground: this.styles.listBackground,1516listBackground: this.styles.listBackground,1517listFocusForeground: this.styles.listForeground,1518listHoverForeground: this.styles.listForeground,1519listInactiveFocusForeground: this.styles.listForeground,1520listInactiveSelectionForeground: this.styles.listForeground,1521listActiveSelectionForeground: this.styles.listForeground,1522listFocusAndSelectionForeground: this.styles.listForeground,1523listActiveSelectionIconForeground: undefined,1524listInactiveSelectionIconForeground: undefined,1525}1526}));1527this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));15281529this._register(this.tree.onDidChangeContentHeight(() => {1530this.onDidChangeTreeContentHeight();1531}));1532this._register(this.renderer.onDidChangeItemHeight(e => {1533if (this.tree.hasElement(e.element)) {1534this.tree.updateElementHeight(e.element, e.height);1535}1536}));1537this._register(this.tree.onDidFocus(() => {1538this._onDidFocus.fire();1539}));1540this._register(this.tree.onDidScroll(() => {1541this._onDidScroll.fire();15421543const isScrolledDown = this.tree.scrollTop >= this.tree.scrollHeight - this.tree.renderHeight - 2;1544this.container.classList.toggle('show-scroll-down', !isScrolledDown && !this.scrollLock);1545}));1546}15471548startEditing(requestId: string): void {1549const editedRequest = this.renderer.getTemplateDataForRequestId(requestId);1550if (editedRequest) {1551this.clickedRequest(editedRequest);1552}1553}15541555private clickedRequest(item: IChatListItemTemplate) {15561557// cancel current request before we start editing.1558if (this.viewModel) {1559this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId);1560}15611562const currentElement = item.currentElement;1563if (isRequestVM(currentElement) && !this.viewModel?.editing) {15641565const requests = this.viewModel?.model.getRequests();1566if (!requests) {1567return;1568}15691570// this will only ever be true if we restored a checkpoint1571if (this.viewModel?.model.checkpoint) {1572this.recentlyRestoredCheckpoint = true;1573}15741575this.viewModel?.model.setCheckpoint(currentElement.id);15761577// set contexts and request to false1578const currentContext: IChatRequestVariableEntry[] = [];1579for (let i = requests.length - 1; i >= 0; i -= 1) {1580const request = requests[i];1581if (request.id === currentElement.id) {1582request.shouldBeBlocked = false; // unblocking just this request.1583if (request.attachedContext) {1584const context = request.attachedContext.filter(entry => !(isPromptFileVariableEntry(entry) || isPromptTextVariableEntry(entry)) || !entry.automaticallyAdded);1585currentContext.push(...context);1586}1587}1588}15891590// set states1591this.viewModel?.setEditing(currentElement);1592if (item?.contextKeyService) {1593ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true);1594}15951596const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';1597this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);15981599if (!isInput) {1600const rowContainer = item.rowContainer;1601this.inputContainer = dom.$('.chat-edit-input-container');1602rowContainer.appendChild(this.inputContainer);1603this.createInput(this.inputContainer);1604this.input.setChatMode(this.inputPart.currentModeKind);1605} else {1606this.inputPart.element.classList.add('editing');1607}16081609this.inputPart.toggleChatInputOverlay(!isInput);1610if (currentContext.length > 0) {1611this.input.attachmentModel.addContext(...currentContext);1612}161316141615// rerenders1616this.inputPart.dnd.setDisabledOverlay(!isInput);1617this.input.renderAttachedContext();1618this.input.setValue(currentElement.messageText, false);1619this.renderer.updateItemHeightOnRender(currentElement, item);1620this.onDidChangeItems();1621this.input.inputEditor.focus();16221623this._register(this.inputPart.onDidClickOverlay(() => {1624if (this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {1625this.finishedEditing();1626}1627}));16281629// listeners1630if (!isInput) {1631this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => {1632this.scrollToCurrentItem(currentElement);1633}));16341635this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => {1636this.scrollToCurrentItem(currentElement);1637}));1638}1639}16401641type StartRequestEvent = { editRequestType: string };16421643type StartRequestEventClassification = {1644owner: 'justschen';1645comment: 'Event used to gain insights into when edits are being pressed.';1646editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1647};16481649this.telemetryService.publicLog2<StartRequestEvent, StartRequestEventClassification>('chat.startEditingRequests', {1650editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1651});1652}16531654finishedEditing(completedEdit?: boolean): void {1655// reset states1656const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1657if (this.recentlyRestoredCheckpoint) {1658this.recentlyRestoredCheckpoint = false;1659} else {1660this.viewModel?.model.setCheckpoint(undefined);1661}1662this.inputPart.dnd.setDisabledOverlay(false);1663if (editedRequest?.contextKeyService) {1664ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false);1665}16661667const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';16681669if (!isInput) {1670this.inputPart.setChatMode(this.input.currentModeKind);1671const currentModel = this.input.selectedLanguageModel;1672if (currentModel) {1673this.inputPart.switchModel(currentModel.metadata);1674}16751676this.inputPart?.toggleChatInputOverlay(false);1677try {1678if (editedRequest?.rowContainer && editedRequest.rowContainer.contains(this.inputContainer)) {1679editedRequest.rowContainer.removeChild(this.inputContainer);1680} else if (this.inputContainer.parentElement) {1681this.inputContainer.parentElement.removeChild(this.inputContainer);1682}1683} catch (e) {1684this.logService.error('Error occurred while finishing editing:', e);1685}1686this.inputContainer = dom.$('.empty-chat-state');16871688// only dispose if we know the input is not the bottom input object.1689this.input.dispose();1690}16911692if (isInput) {1693this.inputPart.element.classList.remove('editing');1694}1695this.viewModel?.setEditing(undefined);16961697this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);16981699this.onDidChangeItems();1700if (editedRequest && editedRequest.currentElement) {1701this.renderer.updateItemHeightOnRender(editedRequest.currentElement, editedRequest);1702}17031704type CancelRequestEditEvent = {1705editRequestType: string;1706editCanceled: boolean;1707};17081709type CancelRequestEventEditClassification = {1710owner: 'justschen';1711editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };1712editCanceled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether the edit was canceled.' };1713comment: 'Event used to gain insights into when edits are being canceled.';1714};17151716this.telemetryService.publicLog2<CancelRequestEditEvent, CancelRequestEventEditClassification>('chat.editRequestsFinished', {1717editRequestType: this.configurationService.getValue<string>('chat.editRequests'),1718editCanceled: !completedEdit1719});17201721this.inputPart.focus();1722}17231724private scrollToCurrentItem(currentElement: IChatRequestViewModel): void {1725if (this.viewModel?.editing && currentElement) {1726const element = currentElement;1727if (!this.tree.hasElement(element)) {1728return;1729}1730const relativeTop = this.tree.getRelativeTop(element);1731if (relativeTop === null || relativeTop < 0 || relativeTop > 1) {1732this.tree.reveal(element, 0);1733}1734}1735}17361737private onContextMenu(e: ITreeContextMenuEvent<ChatTreeItem | null>): void {1738e.browserEvent.preventDefault();1739e.browserEvent.stopPropagation();17401741const selected = e.element;1742const scopedContextKeyService = this.contextKeyService.createOverlay([1743[ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered]1744]);1745this.contextMenuService.showContextMenu({1746menuId: MenuId.ChatContext,1747menuActionOptions: { shouldForwardArgs: true },1748contextKeyService: scopedContextKeyService,1749getAnchor: () => e.anchor,1750getActionsContext: () => selected,1751});1752}17531754private onDidChangeTreeContentHeight(): void {1755// If the list was previously scrolled all the way down, ensure it stays scrolled down, if scroll lock is on1756if (this.tree.scrollHeight !== this.previousTreeScrollHeight) {1757const lastItem = this.viewModel?.getItems().at(-1);1758const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;1759if (!lastResponseIsRendering || this.scrollLock) {1760// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.1761// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.1762const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2;1763if (lastElementWasVisible) {1764dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {1765// Can't set scrollTop during this event listener, the list might overwrite the change17661767this.scrollToEnd();1768}, 0);1769}1770}1771}17721773// TODO@roblourens add `show-scroll-down` class when button should show1774// 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)1775// So for example it would not reappear if I scroll up and delete a message17761777this.previousTreeScrollHeight = this.tree.scrollHeight;1778this._onDidChangeContentHeight.fire();1779}17801781private getWidgetViewKindTag(): string {1782if (!this.viewContext) {1783return 'editor';1784} else if ('viewId' in this.viewContext) {1785return 'view';1786} else {1787return 'quick';1788}1789}17901791private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'compact' | 'minimal' }): void {1792const commonConfig = {1793renderFollowups: options?.renderFollowups ?? true,1794renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle,1795menus: {1796executeToolbar: MenuId.ChatExecute,1797telemetrySource: 'chatWidget',1798...this.viewOptions.menus1799},1800editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,1801enableImplicitContext: this.viewOptions.enableImplicitContext,1802renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit',1803supportsChangingModes: this.viewOptions.supportsChangingModes,1804dndContainer: this.viewOptions.dndContainer,1805widgetViewKindTag: this.getWidgetViewKindTag()1806};18071808if (this.viewModel?.editing) {1809const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1810const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService])));1811this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart,1812this.location,1813commonConfig,1814this.styles,1815() => this.collectInputState(),1816true1817);1818} else {1819this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart,1820this.location,1821commonConfig,1822this.styles,1823() => this.collectInputState(),1824false1825);1826}18271828this.input.render(container, '', this);18291830this._register(this.input.onDidLoadInputState(state => {1831this.contribs.forEach(c => {1832if (c.setInputState) {1833const contribState = (typeof state === 'object' && state?.[c.id]) ?? {};1834c.setInputState(contribState);1835}1836});1837this.refreshParsedInput();1838}));1839this._register(this.input.onDidFocus(() => this._onDidFocus.fire()));1840this._register(this.input.onDidAcceptFollowup(e => {1841if (!this.viewModel) {1842return;1843}18441845let msg = '';1846if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind)?.id) {1847const agent = this.chatAgentService.getAgent(e.followup.agentId);1848if (!agent) {1849return;1850}18511852this.lastSelectedAgent = agent;1853msg = `${chatAgentLeader}${agent.name} `;1854if (e.followup.subCommand) {1855msg += `${chatSubcommandLeader}${e.followup.subCommand} `;1856}1857} else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) {1858msg = `${chatSubcommandLeader}${e.followup.subCommand} `;1859}18601861msg += e.followup.message;1862this.acceptInput(msg);18631864if (!e.response) {1865// Followups can be shown by the welcome message, then there is no response associated.1866// At some point we probably want telemetry for these too.1867return;1868}18691870this.chatService.notifyUserAction({1871sessionId: this.viewModel.sessionId,1872requestId: e.response.requestId,1873agentId: e.response.agent?.id,1874command: e.response.slashCommand?.name,1875result: e.response.result,1876action: {1877kind: 'followUp',1878followup: e.followup1879},1880});1881}));1882this._register(this.input.onDidChangeHeight(() => {1883const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);1884if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) {1885this.renderer.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest);1886}18871888if (this.bodyDimension) {1889this.layout(this.bodyDimension.height, this.bodyDimension.width);1890}18911892this._onDidChangeContentHeight.fire();1893}));1894this._register(this.input.attachmentModel.onDidChange(() => {1895if (this._editingSession) {1896// TODO still needed? Do this inside input part and fire onDidChangeHeight?1897this.renderChatEditingSessionState();1898}1899}));1900this._register(this.inputEditor.onDidChangeModelContent(() => {1901this.parsedChatRequest = undefined;1902this.updateChatInputContext();1903}));1904this._register(this.chatAgentService.onDidChangeAgents(() => {1905this.parsedChatRequest = undefined;1906// Tools agent loads -> welcome content changes1907this.renderWelcomeViewContentIfNeeded();1908}));1909this._register(this.input.onDidChangeCurrentChatMode(() => {1910this.lastWelcomeViewChatMode = this.input.currentModeKind;1911this.renderWelcomeViewContentIfNeeded();1912this.refreshParsedInput();1913this.renderFollowups();1914}));19151916this._register(autorun(r => {1917const toolSetIds = new Set<string>();1918const toolIds = new Set<string>();1919for (const [entry, enabled] of this.input.selectedToolsModel.entriesMap.read(r)) {1920if (enabled) {1921if (entry instanceof ToolSet) {1922toolSetIds.add(entry.id);1923} else {1924toolIds.add(entry.id);1925}1926}1927}1928const disabledTools = this.input.attachmentModel.attachments1929.filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id))1930.map(a => a.id);19311932this.input.attachmentModel.updateContext(disabledTools, Iterable.empty());1933this.refreshParsedInput();1934}));1935}19361937private onDidStyleChange(): void {1938this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? '');1939this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? '');1940this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? '');1941}194219431944setModel(model: IChatModel, viewState: IChatViewState): void {1945if (!this.container) {1946throw new Error('Call render() before setModel()');1947}19481949if (model.sessionId === this.viewModel?.sessionId) {1950return;1951}19521953if (this.historyList) {1954this.historyList.setFocus([]);1955this.historyList.setSelection([]);1956}19571958this._codeBlockModelCollection.clear();19591960this.container.setAttribute('data-session-id', model.sessionId);1961this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection);19621963if (this._lockedToCodingAgent) {1964const placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedToCodingAgent);1965this.viewModel.setInputPlaceholder(placeholder);1966this.inputEditor.updateOptions({ placeholder });1967} else if (this.viewModel.inputPlaceholder) {1968this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });1969}19701971const renderImmediately = this.configurationService.getValue<boolean>('chat.experimental.renderMarkdownImmediately');1972const delay = renderImmediately ? MicrotaskDelay : 0;1973this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, delay), (events => {1974if (!this.viewModel) {1975return;1976}19771978this.requestInProgress.set(this.viewModel.requestInProgress);19791980// Update the editor's placeholder text when it changes in the view model1981if (events?.some(e => e?.kind === 'changePlaceholder')) {1982this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });1983}19841985this.onDidChangeItems();1986if (events?.some(e => e?.kind === 'addRequest') && this.visible) {1987this.scrollToEnd();1988}19891990if (this._editingSession) {1991this.renderChatEditingSessionState();1992}1993})));1994this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => {1995// Ensure that view state is saved here, because we will load it again when a new model is assigned1996this.input.saveState();1997if (this.viewModel?.editing) {1998this.finishedEditing();1999}2000// Disposes the viewmodel and listeners2001this.viewModel = undefined;2002this.onDidChangeItems();2003}));2004this.input.initForNewChatModel(viewState, model.getRequests().length === 0);2005this.contribs.forEach(c => {2006if (c.setInputState && viewState.inputState?.[c.id]) {2007c.setInputState(viewState.inputState?.[c.id]);2008}2009});20102011this.refreshParsedInput();2012this.viewModelDisposables.add(model.onDidChange((e) => {2013if (e.kind === 'setAgent') {2014this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command });2015}2016if (e.kind === 'addRequest') {2017this.chatTodoListWidget.clear(model.sessionId);2018}2019}));20202021if (this.tree && this.visible) {2022this.onDidChangeItems();2023this.scrollToEnd();2024}20252026this.renderer.updateViewModel(this.viewModel);2027this.updateChatInputContext();2028}20292030getFocus(): ChatTreeItem | undefined {2031return this.tree.getFocus()[0] ?? undefined;2032}20332034reveal(item: ChatTreeItem, relativeTop?: number): void {2035this.tree.reveal(item, relativeTop);2036}20372038focus(item: ChatTreeItem): void {2039const items = this.tree.getNode(null).children;2040const node = items.find(i => i.element?.id === item.id);2041if (!node) {2042return;2043}20442045this.tree.setFocus([node.element]);2046this.tree.domFocus();2047}20482049refilter() {2050this.tree.refilter();2051}20522053setInputPlaceholder(placeholder: string): void {2054this.viewModel?.setInputPlaceholder(placeholder);2055}20562057resetInputPlaceholder(): void {2058this.viewModel?.resetInputPlaceholder();2059}20602061setInput(value = ''): void {2062this.input.setValue(value, false);2063this.refreshParsedInput();2064}20652066getInput(): string {2067return this.input.inputEditor.getValue();2068}20692070// Coding agent locking methods2071public lockToCodingAgent(name: string, displayName: string, agentId: string): void {2072this._lockedToCodingAgent = displayName;2073this._codingAgentPrefix = `@${name} `;2074this._lockedAgentId = agentId;2075this._lockedToCodingAgentContextKey.set(true);2076this.renderWelcomeViewContentIfNeeded();2077this.input.setChatMode(ChatModeKind.Ask);2078this.renderer.updateOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true });2079this.tree.rerender();2080}20812082public unlockFromCodingAgent(): void {2083// Clear all state related to locking2084this._lockedToCodingAgent = undefined;2085this._codingAgentPrefix = undefined;2086this._lockedAgentId = undefined;2087this._lockedToCodingAgentContextKey.set(false);20882089// Explicitly update the DOM to reflect unlocked state2090this.renderWelcomeViewContentIfNeeded();20912092// Reset to default placeholder2093if (this.viewModel) {2094this.viewModel.resetInputPlaceholder();2095}2096this.inputEditor.updateOptions({ placeholder: undefined });2097this.renderer.updateOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask });2098this.tree.rerender();2099}21002101public get isLockedToCodingAgent(): boolean {2102return !!this._lockedToCodingAgent;2103}21042105public get lockedAgentId(): string | undefined {2106return this._lockedAgentId;2107}21082109logInputHistory(): void {2110this.input.logInputHistory();2111}21122113async acceptInput(query?: string, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {2114return this._acceptInput(query ? { query } : undefined, options);2115}21162117async rerunLastRequest(): Promise<void> {2118if (!this.viewModel) {2119return;2120}21212122const sessionId = this.viewModel.sessionId;2123const lastRequest = this.chatService.getSession(sessionId)?.getRequests().at(-1);2124if (!lastRequest) {2125return;2126}21272128const options: IChatSendRequestOptions = {2129attempt: lastRequest.attempt + 1,2130location: this.location,2131userSelectedModelId: this.input.currentLanguageModel2132};2133return await this.chatService.resendRequest(lastRequest, options);2134}21352136private collectInputState(): IChatInputState {2137const inputState: IChatInputState = {};2138this.contribs.forEach(c => {2139if (c.getInputState) {2140inputState[c.id] = c.getInputState();2141}2142});21432144return inputState;2145}21462147private _findPromptFileInContext(attachedContext: ChatRequestVariableSet): URI | undefined {2148for (const item of attachedContext.asArray()) {2149if (isPromptFileVariableEntry(item) && item.isRoot && this.promptsService.getPromptFileType(item.value) === PromptsType.prompt) {2150return item.value;2151}2152}2153return undefined;2154}21552156private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise<IPromptParserResult | undefined> {2157if (!PromptsConfig.enabled(this.configurationService)) {2158// if prompts are not enabled, we don't need to do anything2159return undefined;2160}21612162let parseResult: IPromptParserResult | undefined;21632164// first check if the input has a prompt slash command2165const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart);2166if (agentSlashPromptPart) {2167parseResult = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.slashPromptCommand, CancellationToken.None);2168if (parseResult) {2169// add the prompt file to the context, but not sticky2170const toolReferences = this.toolsService.toToolReferences(parseResult.variableReferences);2171requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));21722173// remove the slash command from the input2174requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim();2175}2176} else {2177// if not, check if the context contains a prompt file: This is the old workflow that we still support for legacy reasons2178const uri = this._findPromptFileInContext(requestInput.attachedContext);2179if (uri) {2180try {2181parseResult = await this.promptsService.parse(uri, PromptsType.prompt, CancellationToken.None);2182} catch (error) {2183this.logService.error(`[_applyPromptFileIfSet] Failed to parse prompt file: ${uri}`, error);2184}2185}2186}21872188if (!parseResult) {2189return undefined;2190}2191const meta = parseResult.metadata;2192if (meta?.promptType !== PromptsType.prompt) {2193return undefined;2194}21952196const input = requestInput.input.trim();2197requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`;2198if (input) {2199// if the input is not empty, append it to the prompt2200requestInput.input += `\n${input}`;2201}22022203await this._applyPromptMetadata(meta, requestInput);22042205return parseResult;2206}22072208private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {2209if (this.viewModel?.requestInProgress) {2210return;2211}22122213if (!query && this.input.generating) {2214// if the user submits the input and generation finishes quickly, just submit it for them2215const generatingAutoSubmitWindow = 500;2216const start = Date.now();2217await this.input.generating;2218if (Date.now() - start > generatingAutoSubmitWindow) {2219return;2220}2221}22222223if (this.viewModel) {2224this._onDidAcceptInput.fire();2225this.scrollLock = this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll);22262227const editorValue = this.getInput();2228const requestId = this.chatAccessibilityService.acceptRequest();2229const requestInputs: IChatRequestInputOptions = {2230input: !query ? editorValue : query.query,2231attachedContext: this.input.getAttachedAndImplicitContext(this.viewModel.sessionId),2232};22332234const isUserQuery = !query;22352236if (!this.viewModel.editing) {2237// process the prompt command2238await this._applyPromptFileIfSet(requestInputs);2239await this._autoAttachInstructions(requestInputs);2240}22412242if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) {2243const 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 set2244const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext;22452246// Collect file variables from previous requests before sending the request2247const previousRequests = this.viewModel.model.getRequests();2248for (const request of previousRequests) {2249for (const variable of request.variableData.variables) {2250if (URI.isUri(variable.value) && variable.kind === 'file') {2251const uri = variable.value;2252if (!uniqueWorkingSetEntries.has(uri)) {2253editingSessionAttachedContext.add(variable);2254uniqueWorkingSetEntries.add(variable.value);2255}2256}2257}2258}2259requestInputs.attachedContext = editingSessionAttachedContext;22602261type ChatEditingWorkingSetClassification = {2262owner: 'joyceerhl';2263comment: 'Information about the working set size in a chat editing request';2264originalSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that the user tried to attach in their editing request.' };2265actualSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that were actually sent in their editing request.' };2266};2267type ChatEditingWorkingSetEvent = {2268originalSize: number;2269actualSize: number;2270};2271this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size });2272}22732274this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionId);2275if (this.currentRequest) {2276// 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.2277// This is awkward, it's basically a limitation of the chat provider-based agent.2278await Promise.race([this.currentRequest, timeout(1000)]);2279}22802281this.input.validateAgentMode();22822283if (this.viewModel.model.checkpoint) {2284const requests = this.viewModel.model.getRequests();2285for (let i = requests.length - 1; i >= 0; i -= 1) {2286const request = requests[i];2287if (request.shouldBeBlocked) {2288this.chatService.removeRequest(this.viewModel.sessionId, request.id);2289}2290}2291}22922293const result = await this.chatService.sendRequest(this.viewModel.sessionId, requestInputs.input, {2294userSelectedModelId: this.input.currentLanguageModel,2295location: this.location,2296locationData: this._location.resolveData?.(),2297parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind },2298attachedContext: requestInputs.attachedContext.asArray(),2299noCommandDetection: options?.noCommandDetection,2300...this.getModeRequestOptions(),2301modeInfo: this.input.currentModeInfo,2302agentIdSilent: this._lockedAgentId2303});23042305if (result) {2306this.input.acceptInput(isUserQuery);2307this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });2308this.currentRequest = result.responseCompletePromise.then(() => {2309const responses = this.viewModel?.getItems().filter(isResponseVM);2310const lastResponse = responses?.[responses.length - 1];2311this.chatAccessibilityService.acceptResponse(lastResponse, requestId, options?.isVoiceInput);2312if (lastResponse?.result?.nextQuestion) {2313const { prompt, participant, command } = lastResponse.result.nextQuestion;2314const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command);2315if (question) {2316this.input.setValue(question, false);2317}2318}23192320this.currentRequest = undefined;2321});23222323if (this.viewModel?.editing) {2324this.finishedEditing(true);2325this.viewModel.model?.setCheckpoint(undefined);2326}2327return result.responseCreatedPromise;2328}2329}2330return undefined;2331}23322333getModeRequestOptions(): Partial<IChatSendRequestOptions> {2334return {2335modeInfo: this.input.currentModeInfo,2336userSelectedTools: this.input.selectedToolsModel.userSelectedTools,2337};2338}23392340getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] {2341return this.renderer.getCodeBlockInfosForResponse(response);2342}23432344getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined {2345return this.renderer.getCodeBlockInfoForEditor(uri);2346}23472348getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] {2349return this.renderer.getFileTreeInfosForResponse(response);2350}23512352getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined {2353return this.renderer.getLastFocusedFileTreeForResponse(response);2354}23552356focusLastMessage(): void {2357if (!this.viewModel) {2358return;2359}23602361const items = this.tree.getNode(null).children;2362const lastItem = items[items.length - 1];2363if (!lastItem) {2364return;2365}23662367this.tree.setFocus([lastItem.element]);2368this.tree.domFocus();2369}23702371layout(height: number, width: number): void {2372width = Math.min(width, 950);2373this.bodyDimension = new dom.Dimension(width, height);23742375const layoutHeight = this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height;2376if (this.viewModel?.editing) {2377this.inlineInputPart?.layout(layoutHeight, width);2378}23792380if (this.container.classList.contains('experimental-welcome-view')) {2381this.inputPart.layout(layoutHeight, Math.min(width, 650));2382}2383else {2384this.inputPart.layout(layoutHeight, width);2385}23862387const inputHeight = this.inputPart.inputPartHeight;2388const chatTodoListWidgetHeight = this.chatTodoListWidget.height;2389const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2;23902391const contentHeight = Math.max(0, height - inputHeight - chatTodoListWidgetHeight);2392if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') {2393this.listContainer.style.removeProperty('--chat-current-response-min-height');2394} else {2395this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px');2396}2397this.tree.layout(contentHeight, width);2398this.tree.getHTMLElement().style.height = `${contentHeight}px`;23992400// Push the welcome message down so it doesn't change position2401// when followups, attachments or working set appear2402let welcomeOffset = 100;2403if (this.viewOptions.renderFollowups) {2404welcomeOffset = Math.max(welcomeOffset - this.input.followupsHeight, 0);2405}2406if (this.viewOptions.enableWorkingSet) {2407welcomeOffset = Math.max(welcomeOffset - this.input.editSessionWidgetHeight, 0);2408}2409welcomeOffset = Math.max(welcomeOffset - this.input.attachmentsHeight, 0);2410this.welcomeMessageContainer.style.height = `${contentHeight - welcomeOffset}px`;2411this.welcomeMessageContainer.style.paddingBottom = `${welcomeOffset}px`;24122413this.renderer.layout(width);24142415const lastItem = this.viewModel?.getItems().at(-1);2416const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;2417if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) {2418this.scrollToEnd();2419}2420this.listContainer.style.height = `${contentHeight}px`;24212422this._onDidChangeHeight.fire(height);2423}24242425private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean };24262427// An alternative to layout, this allows you to specify the number of ChatTreeItems2428// you want to show, and the max height of the container. It will then layout the2429// tree to show that many items.2430// TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used2431setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2432this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2433this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode()));24342435const mutableDisposable = this._register(new MutableDisposable());2436this._register(this.tree.onDidScroll((e) => {2437// TODO@TylerLeonhardt this should probably just be disposed when this is disabled2438// and then set up again when it is enabled again2439if (!this._dynamicMessageLayoutData?.enabled) {2440return;2441}2442mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {2443if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) {2444return;2445}2446const renderHeight = e.height;2447const diff = e.scrollHeight - renderHeight - e.scrollTop;2448if (diff === 0) {2449return;2450}24512452const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight);2453const width = this.bodyDimension?.width ?? this.container.offsetWidth;2454this.input.layout(possibleMaxHeight, width);2455const inputPartHeight = this.input.inputPartHeight;2456const chatTodoListWidgetHeight = this.chatTodoListWidget.height;2457const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatTodoListWidgetHeight);2458this.layout(newHeight + inputPartHeight + chatTodoListWidgetHeight, width);2459});2460}));2461}24622463updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {2464this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };2465let hasChanged = false;2466let height = this.bodyDimension!.height;2467let width = this.bodyDimension!.width;2468if (maxHeight < this.bodyDimension!.height) {2469height = maxHeight;2470hasChanged = true;2471}2472const containerWidth = this.container.offsetWidth;2473if (this.bodyDimension?.width !== containerWidth) {2474width = containerWidth;2475hasChanged = true;2476}2477if (hasChanged) {2478this.layout(height, width);2479}2480}24812482get isDynamicChatTreeItemLayoutEnabled(): boolean {2483return this._dynamicMessageLayoutData?.enabled ?? false;2484}24852486set isDynamicChatTreeItemLayoutEnabled(value: boolean) {2487if (!this._dynamicMessageLayoutData) {2488return;2489}2490this._dynamicMessageLayoutData.enabled = value;2491}24922493layoutDynamicChatTreeItemMode(): void {2494if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) {2495return;2496}24972498const width = this.bodyDimension?.width ?? this.container.offsetWidth;2499this.input.layout(this._dynamicMessageLayoutData.maxHeight, width);2500const inputHeight = this.input.inputPartHeight;2501const chatTodoListWidgetHeight = this.chatTodoListWidget.height;25022503const totalMessages = this.viewModel.getItems();2504// grab the last N messages2505const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages);25062507const needsRerender = messages.some(m => m.currentRenderedHeight === undefined);2508const listHeight = needsRerender2509? this._dynamicMessageLayoutData.maxHeight2510: messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0);25112512this.layout(2513Math.min(2514// we add an additional 18px in order to show that there is scrollable content2515inputHeight + chatTodoListWidgetHeight + listHeight + (totalMessages.length > 2 ? 18 : 0),2516this._dynamicMessageLayoutData.maxHeight2517),2518width2519);25202521if (needsRerender || !listHeight) {2522this.scrollToEnd();2523}2524}25252526saveState(): void {2527this.input.saveState();2528}25292530getViewState(): IChatViewState {2531// Get the input state which includes our locked agent (if any)2532const inputState = this.input.getViewState();2533return {2534inputValue: this.getInput(),2535inputState: inputState2536};2537}25382539private updateChatInputContext() {2540const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart);2541this.agentInInput.set(!!currentAgent);2542}25432544private async _applyPromptMetadata(metadata: TPromptMetadata, requestInput: IChatRequestInputOptions): Promise<void> {25452546const { mode, tools, model } = metadata;25472548const currentMode = this.input.currentModeObs.get();25492550// switch to appropriate chat mode if needed2551if (mode && mode !== currentMode.name) {2552// Find the mode object to get its kind2553const chatMode = this.chatModeService.findModeByName(mode);2554if (chatMode) {2555if (currentMode.kind !== chatMode.kind) {2556const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentMode.kind, chatMode.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model.editingSession);2557if (!chatModeCheck) {2558return undefined;2559} else if (chatModeCheck.needToClearSession) {2560this.clear();2561await this.waitForReady();2562}2563}2564this.input.setChatMode(chatMode.id);2565}2566}25672568// if not tools to enable are present, we are done2569if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) {2570const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools);2571this.input.selectedToolsModel.set(enablementMap, true);2572}25732574if (model !== undefined) {2575this.input.switchModelByQualifiedName(model);2576}2577}25782579/**2580* Adds additional instructions to the context2581* - instructions that have a 'applyTo' pattern that matches the current input2582* - instructions referenced in the copilot settings 'copilot-instructions'2583* - instructions referenced in an already included instruction file2584*/2585private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {2586const promptsConfigEnabled = PromptsConfig.enabled(this.configurationService);2587this.logService.debug(`ChatWidget#_autoAttachInstructions: ${PromptsConfig.KEY}: ${promptsConfigEnabled}`);25882589if (promptsConfigEnabled) {2590const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this._getReadTool());2591await computer.collect(attachedContext, CancellationToken.None);2592} else {2593const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, undefined);2594await computer.collectAgentInstructionsOnly(attachedContext, CancellationToken.None);2595}2596}25972598private _getReadTool(): IToolData | undefined {2599if (this.input.currentModeKind !== ChatModeKind.Agent) {2600return undefined;2601}2602const readFileTool = this.toolsService.getToolByName('readFile');2603if (!readFileTool || !this.input.selectedToolsModel.userSelectedTools.get()[readFileTool.id]) {2604return undefined;2605}2606return readFileTool;2607}26082609delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {2610this.tree.delegateScrollFromMouseWheelEvent(browserEvent);2611}2612}26132614export class ChatWidgetService extends Disposable implements IChatWidgetService {26152616declare readonly _serviceBrand: undefined;26172618private _widgets: ChatWidget[] = [];2619private _lastFocusedWidget: ChatWidget | undefined = undefined;26202621private readonly _onDidAddWidget = this._register(new Emitter<ChatWidget>());2622readonly onDidAddWidget: Event<IChatWidget> = this._onDidAddWidget.event;26232624get lastFocusedWidget(): IChatWidget | undefined {2625return this._lastFocusedWidget;2626}26272628getAllWidgets(): ReadonlyArray<IChatWidget> {2629return this._widgets;2630}26312632getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray<IChatWidget> {2633return this._widgets.filter(w => w.location === location);2634}26352636getWidgetByInputUri(uri: URI): ChatWidget | undefined {2637return this._widgets.find(w => isEqual(w.inputUri, uri));2638}26392640getWidgetBySessionId(sessionId: string): ChatWidget | undefined {2641return this._widgets.find(w => w.viewModel?.sessionId === sessionId);2642}26432644private setLastFocusedWidget(widget: ChatWidget | undefined): void {2645if (widget === this._lastFocusedWidget) {2646return;2647}26482649this._lastFocusedWidget = widget;2650}26512652register(newWidget: ChatWidget): IDisposable {2653if (this._widgets.some(widget => widget === newWidget)) {2654throw new Error('Cannot register the same widget multiple times');2655}26562657this._widgets.push(newWidget);2658this._onDidAddWidget.fire(newWidget);26592660return combinedDisposable(2661newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)),2662toDisposable(() => this._widgets.splice(this._widgets.indexOf(newWidget), 1))2663);2664}2665}266626672668