Path: blob/main/src/vs/workbench/contrib/chat/browser/chatInputPart.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 { addDisposableListener } from '../../../../base/browser/dom.js';7import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js';8import { IHistoryNavigationWidget } from '../../../../base/browser/history.js';9import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';10import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';11import * as aria from '../../../../base/browser/ui/aria/aria.js';12import { Button, ButtonWithIcon } from '../../../../base/browser/ui/button/button.js';13import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';14import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';15import { IAction } from '../../../../base/common/actions.js';16import { DeferredPromise } from '../../../../base/common/async.js';17import { CancellationToken } from '../../../../base/common/cancellation.js';18import { Codicon } from '../../../../base/common/codicons.js';19import { Emitter, Event } from '../../../../base/common/event.js';20import { HistoryNavigator2 } from '../../../../base/common/history.js';21import { KeyCode } from '../../../../base/common/keyCodes.js';22import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';23import { ResourceSet } from '../../../../base/common/map.js';24import { Schemas } from '../../../../base/common/network.js';25import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js';26import { isMacintosh } from '../../../../base/common/platform.js';27import { isEqual } from '../../../../base/common/resources.js';28import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';29import { assertType } from '../../../../base/common/types.js';30import { URI } from '../../../../base/common/uri.js';31import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';32import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';33import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';34import { EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js';35import { IDimension } from '../../../../editor/common/core/2d/dimension.js';36import { IPosition } from '../../../../editor/common/core/position.js';37import { Range } from '../../../../editor/common/core/range.js';38import { isLocation } from '../../../../editor/common/languages.js';39import { ITextModel } from '../../../../editor/common/model.js';40import { IModelService } from '../../../../editor/common/services/model.js';41import { ITextModelService } from '../../../../editor/common/services/resolverService.js';42import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js';43import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js';44import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js';45import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js';46import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js';47import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';48import { localize } from '../../../../nls.js';49import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';50import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js';51import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOptions } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';52import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';53import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';54import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';55import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';56import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';57import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';58import { IFileService } from '../../../../platform/files/common/files.js';59import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';60import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';61import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';62import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';63import { ILabelService } from '../../../../platform/label/common/label.js';64import { WorkbenchList } from '../../../../platform/list/browser/listService.js';65import { ILogService } from '../../../../platform/log/common/log.js';66import { INotificationService } from '../../../../platform/notification/common/notification.js';67import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';68import { IThemeService } from '../../../../platform/theme/common/themeService.js';69import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';70import { ResourceLabels } from '../../../browser/labels.js';71import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';72import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';73import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';74import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';75import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js';76import { IChatAgentService } from '../common/chatAgents.js';77import { ChatContextKeys } from '../common/chatContextKeys.js';78import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';79import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js';80import { IChatRequestModeInfo } from '../common/chatModel.js';81import { ChatMode, IChatMode, IChatModeService } from '../common/chatModes.js';82import { IChatFollowup } from '../common/chatService.js';83import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js';84import { IChatResponseViewModel } from '../common/chatViewModel.js';85import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js';86import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../common/constants.js';87import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';88import { ILanguageModelToolsService } from '../common/languageModelToolsService.js';89import { PromptsType } from '../common/promptSyntax/promptTypes.js';90import { IPromptsService } from '../common/promptSyntax/service/promptsService.js';91import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js';92import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js';93import { IChatWidget } from './chat.js';94import { ChatAttachmentModel } from './chatAttachmentModel.js';95import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from './chatAttachmentWidgets.js';96import { IDisposableReference } from './chatContentParts/chatCollections.js';97import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js';98import { ChatDragAndDrop } from './chatDragAndDrop.js';99import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js';100import { ChatFollowups } from './chatFollowups.js';101import { ChatSelectedTools } from './chatSelectedTools.js';102import { IChatViewState } from './chatWidget.js';103import { ChatImplicitContext } from './contrib/chatImplicitContext.js';104import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js';105import { resizeImage } from './imageUtils.js';106import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js';107import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js';108109const $ = dom.$;110111const INPUT_EDITOR_MAX_HEIGHT = 250;112113export interface IChatInputStyles {114overlayBackground: string;115listForeground: string;116listBackground: string;117}118119interface IChatInputPartOptions {120renderFollowups: boolean;121renderStyle?: 'compact';122menus: {123executeToolbar: MenuId;124telemetrySource: string;125inputSideToolbar?: MenuId;126};127editorOverflowWidgetsDomNode?: HTMLElement;128renderWorkingSet?: boolean;129enableImplicitContext?: boolean;130supportsChangingModes?: boolean;131dndContainer?: HTMLElement;132widgetViewKindTag: string;133}134135export interface IWorkingSetEntry {136uri: URI;137}138139const GlobalLastChatModeKey = 'chat.lastChatMode';140141export class ChatInputPart extends Disposable implements IHistoryNavigationWidget {142private static _counter = 0;143144private _workingSetCollapsed = true;145private _lastEditingSessionId: string | undefined;146147private _onDidLoadInputState: Emitter<IChatInputState | undefined>;148readonly onDidLoadInputState: Event<IChatInputState | undefined>;149150private _onDidChangeHeight: Emitter<void>;151readonly onDidChangeHeight: Event<void>;152153private _onDidFocus: Emitter<void>;154readonly onDidFocus: Event<void>;155156private _onDidBlur: Emitter<void>;157readonly onDidBlur: Event<void>;158159private _onDidChangeContext: Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>;160readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>;161162private _onDidAcceptFollowup: Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>;163readonly onDidAcceptFollowup: Event<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>;164165private _onDidClickOverlay: Emitter<void>;166readonly onDidClickOverlay: Event<void>;167168private readonly _attachmentModel: ChatAttachmentModel;169public get attachmentModel(): ChatAttachmentModel {170return this._attachmentModel;171}172173readonly selectedToolsModel: ChatSelectedTools;174175public getAttachedAndImplicitContext(sessionId: string): ChatRequestVariableSet {176177const contextArr = new ChatRequestVariableSet();178179contextArr.add(...this.attachmentModel.attachments);180181if ((this.implicitContext?.enabled && this.implicitContext?.value) || (isLocation(this.implicitContext?.value) && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext'))) {182const implicitChatVariables = this.implicitContext.toBaseEntries();183contextArr.add(...implicitChatVariables);184}185return contextArr;186}187188/**189* Check if the chat input part has any prompt file attachments.190*/191get hasPromptFileAttachments(): boolean {192return this._attachmentModel.attachments.some(entry => {193return isPromptFileVariableEntry(entry) && entry.isRoot && this.promptsService.getPromptFileType(entry.value) === PromptsType.prompt;194});195}196197private _indexOfLastAttachedContextDeletedWithKeyboard: number;198private _indexOfLastOpenedContext: number;199200private _implicitContext: ChatImplicitContext | undefined;201public get implicitContext(): ChatImplicitContext | undefined {202return this._implicitContext;203}204205private _relatedFiles: ChatRelatedFiles | undefined;206public get relatedFiles(): ChatRelatedFiles | undefined {207return this._relatedFiles;208}209210private _hasFileAttachmentContextKey: IContextKey<boolean>;211212private readonly _onDidChangeVisibility: Emitter<boolean>;213private readonly _contextResourceLabels: ResourceLabels;214215private readonly inputEditorMaxHeight: number;216private inputEditorHeight: number;217private container!: HTMLElement;218219private inputSideToolbarContainer?: HTMLElement;220221private followupsContainer!: HTMLElement;222private readonly followupsDisposables: DisposableStore;223224private attachmentsContainer!: HTMLElement;225226private chatInputOverlay!: HTMLElement;227private readonly overlayClickListener: MutableDisposable<IDisposable>;228229private attachedContextContainer!: HTMLElement;230private readonly attachedContextDisposables: MutableDisposable<DisposableStore>;231232private relatedFilesContainer!: HTMLElement;233234private chatEditingSessionWidgetContainer!: HTMLElement;235236private _inputPartHeight: number;237get inputPartHeight() {238return this._inputPartHeight;239}240241private _followupsHeight: number;242get followupsHeight() {243return this._followupsHeight;244}245246private _editSessionWidgetHeight: number;247get editSessionWidgetHeight() {248return this._editSessionWidgetHeight;249}250251get attachmentsHeight() {252return this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0);253}254255private _inputEditor!: CodeEditorWidget;256private _inputEditorElement!: HTMLElement;257258private executeToolbar!: MenuWorkbenchToolBar;259private inputActionsToolbar!: MenuWorkbenchToolBar;260261private addFilesToolbar: MenuWorkbenchToolBar | undefined;262263get inputEditor() {264return this._inputEditor;265}266267readonly dnd: ChatDragAndDrop;268269private history: HistoryNavigator2<IChatHistoryEntry>;270private historyNavigationBackwardsEnablement!: IContextKey<boolean>;271private historyNavigationForewardsEnablement!: IContextKey<boolean>;272private inputModel: ITextModel | undefined;273private inputEditorHasText: IContextKey<boolean>;274private chatCursorAtTop: IContextKey<boolean>;275private inputEditorHasFocus: IContextKey<boolean>;276private currentlyEditingInputKey!: IContextKey<boolean>;277/**278* Context key is set when prompt instructions are attached.279*/280private promptFileAttached: IContextKey<boolean>;281private chatModeKindKey: IContextKey<ChatModeKind>;282283private modelWidget: ModelPickerActionItem | undefined;284private modeWidget: ModePickerActionItem | undefined;285private readonly _waitForPersistedLanguageModel: MutableDisposable<IDisposable>;286private _onDidChangeCurrentLanguageModel: Emitter<ILanguageModelChatMetadataAndIdentifier>;287288private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined;289290get currentLanguageModel() {291return this._currentLanguageModel?.identifier;292}293294get selectedLanguageModel(): ILanguageModelChatMetadataAndIdentifier | undefined {295return this._currentLanguageModel;296}297298private _onDidChangeCurrentChatMode: Emitter<void>;299readonly onDidChangeCurrentChatMode: Event<void>;300301private readonly _currentModeObservable = observableValue<IChatMode>('currentMode', ChatMode.Ask);302public get currentModeKind(): ChatModeKind {303const mode = this._currentModeObservable.get();304return mode.kind === ChatModeKind.Agent && !this.agentService.hasToolsAgent ?305ChatModeKind.Edit :306mode.kind;307}308309public get currentModeObs(): IObservable<IChatMode> {310return this._currentModeObservable;311}312313public get currentModeInfo(): IChatRequestModeInfo {314const mode = this._currentModeObservable.get();315const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom';316317return {318kind: this.currentModeKind,319isBuiltin: mode.isBuiltin,320instructions: {321content: mode.body?.get(),322toolReferences: mode.variableReferences ? this.toolService.toToolReferences(mode.variableReferences.get()) : undefined323},324modeId: modeId,325applyCodeBlockSuggestionId: undefined,326};327}328329private cachedDimensions: dom.Dimension | undefined;330private cachedExecuteToolbarWidth: number | undefined;331private cachedInputToolbarWidth: number | undefined;332333readonly inputUri: URI;334335private _workingSetLinesAddedSpan?: HTMLElement;336private _workingSetLinesRemovedSpan?: HTMLElement;337338private readonly _chatEditsActionsDisposables: DisposableStore;339private readonly _chatEditsDisposables: DisposableStore;340private _chatEditsListPool: CollapsibleListPool;341private _chatEditList: IDisposableReference<WorkbenchList<IChatCollapsibleListItem>> | undefined;342get selectedElements(): URI[] {343const edits = [];344const editsList = this._chatEditList?.object;345const selectedElements = editsList?.getSelectedElements() ?? [];346for (const element of selectedElements) {347if (element.kind === 'reference' && URI.isUri(element.reference)) {348edits.push(element.reference);349}350}351return edits;352}353354private _attemptedWorkingSetEntriesCount: number;355/**356* The number of working set entries that the user actually wanted to attach.357* This is less than or equal to {@link ChatInputPart.chatEditWorkingSetFiles}.358*/359public get attemptedWorkingSetEntriesCount() {360return this._attemptedWorkingSetEntriesCount;361}362363private readonly getInputState: () => IChatInputState;364365/**366* Number consumers holding the 'generating' lock.367*/368private _generating?: { rc: number; defer: DeferredPromise<void> };369370constructor(371// private readonly editorOptions: ChatEditorOptions, // TODO this should be used372private readonly location: ChatAgentLocation,373private readonly options: IChatInputPartOptions,374styles: IChatInputStyles,375getContribsInputState: () => any,376private readonly inline: boolean,377@IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService,378@IModelService private readonly modelService: IModelService,379@IInstantiationService private readonly instantiationService: IInstantiationService,380@IContextKeyService private readonly contextKeyService: IContextKeyService,381@IConfigurationService private readonly configurationService: IConfigurationService,382@IKeybindingService private readonly keybindingService: IKeybindingService,383@IAccessibilityService private readonly accessibilityService: IAccessibilityService,384@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,385@ILogService private readonly logService: ILogService,386@IFileService private readonly fileService: IFileService,387@IEditorService private readonly editorService: IEditorService,388@IThemeService private readonly themeService: IThemeService,389@ITextModelService private readonly textModelResolverService: ITextModelService,390@IStorageService private readonly storageService: IStorageService,391@ILabelService private readonly labelService: ILabelService,392@IChatAgentService private readonly agentService: IChatAgentService,393@ISharedWebContentExtractorService private readonly sharedWebExtracterService: ISharedWebContentExtractorService,394@IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService,395@IChatEntitlementService private readonly entitlementService: IChatEntitlementService,396@IChatModeService private readonly chatModeService: IChatModeService,397@IPromptsService private readonly promptsService: IPromptsService,398@ILanguageModelToolsService private readonly toolService: ILanguageModelToolsService,399) {400super();401this._onDidLoadInputState = this._register(new Emitter<any>());402this.onDidLoadInputState = this._onDidLoadInputState.event;403this._onDidChangeHeight = this._register(new Emitter<void>());404this.onDidChangeHeight = this._onDidChangeHeight.event;405this._onDidFocus = this._register(new Emitter<void>());406this.onDidFocus = this._onDidFocus.event;407this._onDidBlur = this._register(new Emitter<void>());408this.onDidBlur = this._onDidBlur.event;409this._onDidClickOverlay = this._register(new Emitter<void>());410this.onDidClickOverlay = this._onDidClickOverlay.event;411this._onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>());412this.onDidChangeContext = this._onDidChangeContext.event;413this._onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>());414this.onDidAcceptFollowup = this._onDidAcceptFollowup.event;415this._indexOfLastAttachedContextDeletedWithKeyboard = -1;416this._indexOfLastOpenedContext = -1;417this._onDidChangeVisibility = this._register(new Emitter<boolean>());418this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }));419this.inputEditorHeight = 0;420this.followupsDisposables = this._register(new DisposableStore());421this.attachedContextDisposables = this._register(new MutableDisposable<DisposableStore>());422this.overlayClickListener = this._register(new MutableDisposable<IDisposable>());423this._inputPartHeight = 0;424this._followupsHeight = 0;425this._editSessionWidgetHeight = 0;426this._waitForPersistedLanguageModel = this._register(new MutableDisposable<IDisposable>());427this._onDidChangeCurrentLanguageModel = this._register(new Emitter<ILanguageModelChatMetadataAndIdentifier>());428this._onDidChangeCurrentChatMode = this._register(new Emitter<void>());429this.onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event;430this._currentModeObservable.set(ChatMode.Ask, undefined);431this.inputUri = URI.parse(`${Schemas.vscodeChatInput}:input-${ChatInputPart._counter++}`);432this._chatEditsActionsDisposables = this._register(new DisposableStore());433this._chatEditsDisposables = this._register(new DisposableStore());434this._attemptedWorkingSetEntriesCount = 0;435436this._register(this.editorService.onDidActiveEditorChange(() => {437this._indexOfLastOpenedContext = -1;438}));439440this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel));441this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, this.currentModeObs));442this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this._attachmentModel, styles));443444this.getInputState = (): IChatInputState => {445return {446...getContribsInputState(),447chatContextAttachments: this._attachmentModel.attachments,448chatMode: this._currentModeObservable.get().id,449};450};451this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT;452453this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService);454this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService);455this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService);456this.promptFileAttached = ChatContextKeys.hasPromptFile.bindTo(contextKeyService);457this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService);458const chatToolCount = ChatContextKeys.chatToolCount.bindTo(contextKeyService);459460this._register(autorun(reader => {461let count = 0;462const userSelectedTools = this.selectedToolsModel.userSelectedTools.read(reader);463for (const key in userSelectedTools) {464if (userSelectedTools[key] === true) {465count++;466}467}468469chatToolCount.set(count);470}));471472this.history = this.loadHistory();473this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2<IChatHistoryEntry>([{ text: '', state: this.getInputState() }], ChatInputHistoryMaxEntries, historyKeyFn)));474475this._register(this.configurationService.onDidChangeConfiguration(e => {476const newOptions: IEditorOptions = {};477if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) {478newOptions.ariaLabel = this._getAriaLabel();479}480if (e.affectsConfiguration('editor.wordSegmenterLocales')) {481newOptions.wordSegmenterLocales = this.configurationService.getValue<string | string[]>('editor.wordSegmenterLocales');482}483484this.inputEditor.updateOptions(newOptions);485}));486487this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar, { verticalScrollMode: ScrollbarVisibility.Visible }));488489this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService);490491this.initSelectedModel();492493this._register(this.onDidChangeCurrentChatMode(() => {494this.accessibilityService.alert(this._currentModeObservable.get().label);495if (this._inputEditor) {496this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() });497}498499if (this.implicitContext && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext')) {500this.implicitContext.enabled = this._currentModeObservable.get() !== ChatMode.Agent;501}502}));503this._register(this._onDidChangeCurrentLanguageModel.event(() => {504if (this._currentLanguageModel?.metadata.name) {505this.accessibilityService.alert(this._currentLanguageModel.metadata.name);506}507}));508this._register(this.chatModeService.onDidChangeChatModes(() => this.validateCurrentChatMode()));509this._register(autorun(r => {510const mode = this._currentModeObservable.read(r);511const model = mode.model?.read(r);512if (model) {513this.switchModelByQualifiedName(model);514}515}));516}517518private getSelectedModelStorageKey(): string {519return `chat.currentLanguageModel.${this.location}`;520}521522private getSelectedModelIsDefaultStorageKey(): string {523return `chat.currentLanguageModel.${this.location}.isDefault`;524}525526private initSelectedModel() {527let persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION);528if (persistedSelection && persistedSelection.startsWith('github.copilot-chat/')) {529// Convert the persisted selection to make it backwards comptabile with the old LM API. TODO @lramos15 - Remove this after a bit530persistedSelection = persistedSelection.replace('github.copilot-chat/', 'copilot/');531this.storageService.store(this.getSelectedModelStorageKey(), persistedSelection, StorageScope.APPLICATION, StorageTarget.USER);532}533const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'copilot/gpt-4.1');534535if (persistedSelection) {536const model = this.languageModelsService.lookupLanguageModel(persistedSelection);537if (model) {538// Only restore the model if it wasn't the default at the time of storing or it is now the default539if (!persistedAsDefault || model.isDefault) {540this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection });541this.checkModelSupported();542}543} else {544this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => {545const persistedModel = this.languageModelsService.lookupLanguageModel(persistedSelection);546if (persistedModel) {547this._waitForPersistedLanguageModel.clear();548549// Only restore the model if it wasn't the default at the time of storing or it is now the default550if (!persistedAsDefault || persistedModel.isDefault) {551if (persistedModel.isUserSelectable) {552this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection });553this.checkModelSupported();554}555}556}557});558}559}560561this._register(this._onDidChangeCurrentChatMode.event(() => {562this.checkModelSupported();563}));564this._register(this.configurationService.onDidChangeConfiguration(e => {565if (e.affectsConfiguration(ChatConfiguration.Edits2Enabled)) {566this.checkModelSupported();567}568}));569}570571public setEditing(enabled: boolean) {572this.currentlyEditingInputKey?.set(enabled);573}574575public switchModel(modelMetadata: Pick<ILanguageModelChatMetadata, 'vendor' | 'id' | 'family'>) {576const models = this.getModels();577const model = models.find(m => m.metadata.vendor === modelMetadata.vendor && m.metadata.id === modelMetadata.id && m.metadata.family === modelMetadata.family);578if (model) {579this.setCurrentLanguageModel(model);580}581}582583public switchModelByQualifiedName(qualifiedModelName: string): boolean {584const models = this.getModels();585const model = models.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedModelName, m.metadata));586if (model) {587this.setCurrentLanguageModel(model);588return true;589}590return false;591}592593public switchToNextModel(): void {594const models = this.getModels();595if (models.length > 0) {596const currentIndex = models.findIndex(model => model.identifier === this._currentLanguageModel?.identifier);597const nextIndex = (currentIndex + 1) % models.length;598this.setCurrentLanguageModel(models[nextIndex]);599}600}601602public openModelPicker(): void {603this.modelWidget?.show();604}605606public openModePicker(): void {607this.modeWidget?.show();608}609610public setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) {611this._currentLanguageModel = model;612613if (this.cachedDimensions) {614// For quick chat and editor chat, relayout because the input may need to shrink to accomodate the model name615this.layout(this.cachedDimensions.height, this.cachedDimensions.width);616}617618this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER);619this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefault, StorageScope.APPLICATION, StorageTarget.USER);620621this._onDidChangeCurrentLanguageModel.fire(model);622}623624private checkModelSupported(): void {625if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) {626this.setCurrentLanguageModelToDefault();627}628}629630/**631* By ID- prefer this method632*/633setChatMode(mode: ChatModeKind | string, storeSelection = true): void {634if (!this.options.supportsChangingModes) {635return;636}637638const mode2 = this.chatModeService.findModeById(mode) ??639this.chatModeService.findModeById(ChatModeKind.Agent) ??640ChatMode.Ask;641this.setChatMode2(mode2, storeSelection);642}643644private setChatMode2(mode: IChatMode, storeSelection = true): void {645if (!this.options.supportsChangingModes) {646return;647}648649this._currentModeObservable.set(mode, undefined);650this.chatModeKindKey.set(mode.kind);651this._onDidChangeCurrentChatMode.fire();652653if (storeSelection) {654this.storageService.store(GlobalLastChatModeKey, mode.kind, StorageScope.APPLICATION, StorageTarget.USER);655}656}657658private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean {659// Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex660if (this.currentModeKind === ChatModeKind.Agent || (this.currentModeKind === ChatModeKind.Edit && this.configurationService.getValue(ChatConfiguration.Edits2Enabled))) {661return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata);662}663664return true;665}666667private getModels(): ILanguageModelChatMetadataAndIdentifier[] {668const models = this.languageModelsService.getLanguageModelIds()669.map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! }))670.filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry));671models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));672return models;673}674675private setCurrentLanguageModelToDefault() {676const defaultLanguageModelId = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault);677const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => {678const model = this.languageModelsService.lookupLanguageModel(id);679return model?.isUserSelectable && !model.isDefault;680});681const defaultModel = hasUserSelectableLanguageModels && defaultLanguageModelId ?682{ metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } :683undefined;684if (defaultModel) {685this.setCurrentLanguageModel(defaultModel);686}687}688689private loadHistory(): HistoryNavigator2<IChatHistoryEntry> {690const history = this.historyService.getHistory(this.location);691if (history.length === 0) {692history.push({ text: '', state: this.getInputState() });693}694695return new HistoryNavigator2(history, 50, historyKeyFn);696}697698private _getAriaLabel(): string {699const verbose = this.configurationService.getValue<boolean>(AccessibilityVerbositySettingId.Chat);700let kbLabel;701if (verbose) {702kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();703}704let modeLabel = '';705switch (this.currentModeKind) {706case ChatModeKind.Agent:707modeLabel = localize('chatInput.mode.agent', "(Agent Mode), edit files in your workspace.");708break;709case ChatModeKind.Edit:710modeLabel = localize('chatInput.mode.edit', "(Edit Mode), edit files in your workspace.");711break;712case ChatModeKind.Ask:713default:714modeLabel = localize('chatInput.mode.ask', "(Ask Mode), ask questions or type / for topics.");715break;716}717if (verbose) {718return kbLabel719? localize('actions.chat.accessibiltyHelp', "Chat Input {0} Press Enter to send out the request. Use {1} for Chat Accessibility Help.", modeLabel, kbLabel)720: localize('chatInput.accessibilityHelpNoKb', "Chat Input {0} Press Enter to send out the request. Use the Chat Accessibility Help command for more information.", modeLabel);721} else {722return localize('chatInput.accessibilityHelp', "Chat Input {0}.", modeLabel);723}724}725726private validateCurrentChatMode() {727const currentMode = this._currentModeObservable.get();728const validMode = this.chatModeService.findModeById(currentMode.id);729if (!validMode) {730this.setChatMode(ChatModeKind.Agent);731return;732}733}734735initForNewChatModel(state: IChatViewState, modelIsEmpty: boolean): void {736this.history = this.loadHistory();737this.history.add({738text: state.inputValue ?? this.history.current().text,739state: state.inputState ?? this.getInputState()740});741const attachments = state.inputState?.chatContextAttachments ?? [];742this._attachmentModel.clearAndSetContext(...attachments);743744this.selectedToolsModel.resetSessionEnablementState();745746if (state.inputValue) {747this.setValue(state.inputValue, false);748}749750if (state.inputState?.chatMode) {751if (typeof state.inputState.chatMode === 'string') {752this.setChatMode(state.inputState.chatMode);753} else {754// This path is deprecated, but handle old state755this.setChatMode(state.inputState.chatMode.id);756}757} else {758const persistedMode = this.storageService.get(GlobalLastChatModeKey, StorageScope.APPLICATION);759if (persistedMode) {760this.setChatMode(persistedMode);761}762}763764// TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed.765if (modelIsEmpty) {766const storageKey = this.getDefaultModeExperimentStorageKey();767const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false);768if (!hasSetDefaultMode) {769const defaultModeKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultModeFree' : 'chat.defaultMode';770const defaultLanguageModelKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultLanguageModelFree' : 'chat.defaultLanguageModel';771Promise.all([772this.experimentService.getTreatment(defaultModeKey),773this.experimentService.getTreatment(defaultLanguageModelKey),774]).then(([defaultModeTreatment, defaultLanguageModelTreatment]) => {775if (typeof defaultModeTreatment === 'string') {776this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);777const defaultMode = validateChatMode(defaultModeTreatment);778if (defaultMode) {779this.logService.trace(`Applying default mode from experiment: ${defaultMode}`);780this.setChatMode(defaultMode, false);781this.checkModelSupported();782}783}784785if (typeof defaultLanguageModelTreatment === 'string' && this._currentModeObservable.get().kind === ChatModeKind.Agent) {786this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);787this.logService.trace(`Applying default language model from experiment: ${defaultLanguageModelTreatment}`);788this.setExpModelOrWait(defaultLanguageModelTreatment);789}790});791}792}793}794795private setExpModelOrWait(modelId: string) {796const model = this.languageModelsService.lookupLanguageModel(modelId);797if (model) {798this.setCurrentLanguageModel({ metadata: model, identifier: modelId });799this.checkModelSupported();800this._waitForPersistedLanguageModel.clear();801} else {802this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(() => {803const model = this.languageModelsService.lookupLanguageModel(modelId);804if (model) {805this._waitForPersistedLanguageModel.clear();806807if (model.isUserSelectable) {808this.setCurrentLanguageModel({ metadata: model, identifier: modelId });809this.checkModelSupported();810}811}812});813}814}815816private getDefaultModeExperimentStorageKey(): string {817const tag = this.options.widgetViewKindTag;818return `chat.${tag}.hasSetDefaultModeByExperiment`;819}820821logInputHistory(): void {822const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n');823this.logService.info(`[${this.location}] Chat input history:`, historyStr);824}825826setVisible(visible: boolean): void {827this._onDidChangeVisibility.fire(visible);828}829830/** If consumers are busy generating the chat input, returns the promise resolved when they finish */831get generating() {832return this._generating?.defer.p;833}834835/** Disables the input submissions buttons until the disposable is disposed. */836startGenerating(): IDisposable {837this.logService.trace('ChatWidget#startGenerating');838if (this._generating) {839this._generating.rc++;840} else {841this._generating = { rc: 1, defer: new DeferredPromise<void>() };842}843844return toDisposable(() => {845this.logService.trace('ChatWidget#doneGenerating');846if (this._generating && !--this._generating.rc) {847this._generating.defer.complete();848this._generating = undefined;849}850});851}852853get element(): HTMLElement {854return this.container;855}856857async showPreviousValue(): Promise<void> {858const inputState = this.getInputState();859if (this.history.isAtEnd()) {860this.saveCurrentValue(inputState);861} else {862const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);863if (!this.history.has(currentEntry)) {864this.saveCurrentValue(inputState);865this.history.resetCursor();866}867}868869this.navigateHistory(true);870}871872async showNextValue(): Promise<void> {873const inputState = this.getInputState();874if (this.history.isAtEnd()) {875return;876} else {877const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);878if (!this.history.has(currentEntry)) {879this.saveCurrentValue(inputState);880this.history.resetCursor();881}882}883884this.navigateHistory(false);885}886887private async navigateHistory(previous: boolean): Promise<void> {888const historyEntry = previous ?889this.history.previous() : this.history.next();890891let historyAttachments = historyEntry.state?.chatContextAttachments ?? [];892893// Check for images in history to restore the value.894if (historyAttachments.length > 0) {895historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => {896if (isImageVariableEntry(attachment) && attachment.references?.length && URI.isUri(attachment.references[0].reference)) {897const currReference = attachment.references[0].reference;898try {899const imageBinary = currReference.toString(true).startsWith('http') ? await this.sharedWebExtracterService.readImage(currReference, CancellationToken.None) : (await this.fileService.readFile(currReference)).value;900if (!imageBinary) {901return undefined;902}903const newAttachment = { ...attachment };904newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? imageBinary.buffer : await resizeImage(imageBinary.buffer); // if pasted image, we do not need to resize.905return newAttachment;906} catch (err) {907this.logService.error('Failed to fetch and reference.', err);908return undefined;909}910}911return attachment;912}))).filter(attachment => attachment !== undefined);913}914915this._attachmentModel.clearAndSetContext(...historyAttachments);916917aria.status(historyEntry.text);918this.setValue(historyEntry.text, true);919920this._onDidLoadInputState.fire(historyEntry.state);921922const model = this._inputEditor.getModel();923if (!model) {924return;925}926927if (previous) {928const endOfFirstViewLine = this._inputEditor._getViewModel()?.getLineLength(1) ?? 1;929const endOfFirstModelLine = model.getLineLength(1);930if (endOfFirstViewLine === endOfFirstModelLine) {931// Not wrapped - set cursor to the end of the first line932this._inputEditor.setPosition({ lineNumber: 1, column: endOfFirstViewLine + 1 });933} else {934// Wrapped - set cursor one char short of the end of the first view line.935// If it's after the next character, the cursor shows on the second line.936this._inputEditor.setPosition({ lineNumber: 1, column: endOfFirstViewLine });937}938} else {939this._inputEditor.setPosition(getLastPosition(model));940}941}942943setValue(value: string, transient: boolean): void {944this.inputEditor.setValue(value);945// always leave cursor at the end946this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });947948if (!transient) {949this.saveCurrentValue(this.getInputState());950}951}952953private saveCurrentValue(inputState: IChatInputState): void {954const newEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);955this.history.replaceLast(newEntry);956}957958focus() {959this._inputEditor.focus();960}961962hasFocus(): boolean {963return this._inputEditor.hasWidgetFocus();964}965966/**967* Reset the input and update history.968* @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed.969*/970async acceptInput(isUserQuery?: boolean): Promise<void> {971if (isUserQuery) {972const userQuery = this._inputEditor.getValue();973const inputState = this.getInputState();974const entry = this.getFilteredEntry(userQuery, inputState);975this.history.replaceLast(entry);976this.history.add({ text: '' });977}978979// Clear attached context, fire event to clear input state, and clear the input editor980this.attachmentModel.clear();981this._onDidLoadInputState.fire({});982if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) {983this._acceptInputForVoiceover();984} else {985this._inputEditor.focus();986this._inputEditor.setValue('');987}988}989990validateAgentMode(): void {991if (!this.agentService.hasToolsAgent && this._currentModeObservable.get().kind === ChatModeKind.Agent) {992this.setChatMode(ChatModeKind.Edit);993}994}995996// A function that filters out specifically the `value` property of the attachment.997private getFilteredEntry(query: string, inputState: IChatInputState): IChatHistoryEntry {998const attachmentsWithoutImageValues = inputState.chatContextAttachments?.map(attachment => {999if (isImageVariableEntry(attachment) && attachment.references?.length && attachment.value) {1000const newAttachment = { ...attachment };1001newAttachment.value = undefined;1002return newAttachment;1003}1004return attachment;1005});10061007inputState.chatContextAttachments = attachmentsWithoutImageValues;1008const newEntry = {1009text: query,1010state: inputState,1011};10121013return newEntry;1014}10151016private _acceptInputForVoiceover(): void {1017const domNode = this._inputEditor.getDomNode();1018if (!domNode) {1019return;1020}1021// Remove the input editor from the DOM temporarily to prevent VoiceOver1022// from reading the cleared text (the request) to the user.1023domNode.remove();1024this._inputEditor.setValue('');1025this._inputEditorElement.appendChild(domNode);1026this._inputEditor.focus();1027}10281029private _handleAttachedContextChange() {1030this._hasFileAttachmentContextKey.set(Boolean(this._attachmentModel.attachments.find(a => a.kind === 'file')));1031this.renderAttachedContext();1032}10331034render(container: HTMLElement, initialValue: string, widget: IChatWidget) {1035let elements;1036if (this.options.renderStyle === 'compact') {1037elements = dom.h('.interactive-input-part', [1038dom.h('.interactive-input-and-edit-session', [1039dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),1040dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [1041dom.h('.chat-input-container@inputContainer', [1042dom.h('.chat-editor-container@editorContainer'),1043dom.h('.chat-input-toolbars@inputToolbars'),1044]),1045]),1046dom.h('.chat-attachments-container@attachmentsContainer', [1047dom.h('.chat-attachment-toolbar@attachmentToolbar'),1048dom.h('.chat-attached-context@attachedContextContainer'),1049dom.h('.chat-related-files@relatedFilesContainer'),1050]),1051dom.h('.interactive-input-followups@followupsContainer'),1052])1053]);1054} else {1055elements = dom.h('.interactive-input-part', [1056dom.h('.interactive-input-followups@followupsContainer'),1057dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),1058dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [1059dom.h('.chat-input-container@inputContainer', [1060dom.h('.chat-attachments-container@attachmentsContainer', [1061dom.h('.chat-attachment-toolbar@attachmentToolbar'),1062dom.h('.chat-related-files@relatedFilesContainer'),1063dom.h('.chat-attached-context@attachedContextContainer'),1064]),1065dom.h('.chat-editor-container@editorContainer'),1066dom.h('.chat-input-toolbars@inputToolbars'),1067]),1068]),1069]);1070}1071this.container = elements.root;1072this.chatInputOverlay = dom.$('.chat-input-overlay');1073container.append(this.chatInputOverlay);1074container.append(this.container);1075this.container.classList.toggle('compact', this.options.renderStyle === 'compact');1076this.followupsContainer = elements.followupsContainer;1077const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right1078const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars1079const editorContainer = elements.editorContainer;1080this.attachmentsContainer = elements.attachmentsContainer;1081this.attachedContextContainer = elements.attachedContextContainer;1082this.relatedFilesContainer = elements.relatedFilesContainer;1083const toolbarsContainer = elements.inputToolbars;1084const attachmentToolbarContainer = elements.attachmentToolbar;1085this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;1086if (this.options.enableImplicitContext) {1087this._implicitContext = this._register(1088this.instantiationService.createInstance(ChatImplicitContext),1089);10901091this._register(this._implicitContext.onDidChangeValue(() => {1092this._indexOfLastAttachedContextDeletedWithKeyboard = -1;1093this._handleAttachedContextChange();1094}));1095}10961097this.renderAttachedContext();1098this._register(this._attachmentModel.onDidChange((e) => {1099if (e.added.length > 0) {1100this._indexOfLastAttachedContextDeletedWithKeyboard = -1;1101}1102this._handleAttachedContextChange();1103}));1104this.renderChatEditingSessionState(null);11051106if (this.options.renderWorkingSet) {1107this._relatedFiles = this._register(new ChatRelatedFiles());1108this._register(this._relatedFiles.onDidChange(() => this.renderChatRelatedFiles()));1109}1110this.renderChatRelatedFiles();11111112this.dnd.addOverlay(this.options.dndContainer ?? container, this.options.dndContainer ?? container);11131114const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer));1115ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true);1116this.currentlyEditingInputKey = ChatContextKeys.currentlyEditingInput.bindTo(inputScopedContextKeyService);1117const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])));11181119const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this));1120this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement;1121this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement;11221123const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService);1124options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode;1125options.pasteAs = EditorOptions.pasteAs.defaultValue;1126options.readOnly = false;1127options.ariaLabel = this._getAriaLabel();1128options.fontFamily = DEFAULT_FONT_FAMILY;1129options.fontSize = 13;1130options.lineHeight = 20;1131options.padding = this.options.renderStyle === 'compact' ? { top: 2, bottom: 2 } : { top: 8, bottom: 8 };1132options.cursorWidth = 1;1133options.wrappingStrategy = 'advanced';1134options.bracketPairColorization = { enabled: false };1135options.suggest = {1136showIcons: true,1137showSnippets: false,1138showWords: true,1139showStatusBar: false,1140insertMode: 'insert',1141};1142options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' };1143options.stickyScroll = { enabled: false };11441145this._inputEditorElement = dom.append(editorContainer!, $(chatInputEditorContainerSelector));1146const editorOptions = getSimpleCodeEditorWidgetOptions();1147editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID]));1148this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions));11491150SuggestController.get(this._inputEditor)?.forceRenderingAbove();1151options.overflowWidgetsDomNode?.classList.add('hideSuggestTextIcons');1152this._inputEditorElement.classList.add('hideSuggestTextIcons');11531154this._register(this._inputEditor.onDidChangeModelContent(() => {1155const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight);1156if (currentHeight !== this.inputEditorHeight) {1157this.inputEditorHeight = currentHeight;1158this._onDidChangeHeight.fire();1159}11601161const model = this._inputEditor.getModel();1162const inputHasText = !!model && model.getValue().trim().length > 0;1163this.inputEditorHasText.set(inputHasText);1164}));1165this._register(this._inputEditor.onDidContentSizeChange(e => {1166if (e.contentHeightChanged) {1167this.inputEditorHeight = !this.inline ? e.contentHeight : this.inputEditorHeight;1168this._onDidChangeHeight.fire();1169}1170}));1171this._register(this._inputEditor.onDidFocusEditorText(() => {1172this.inputEditorHasFocus.set(true);1173this._onDidFocus.fire();1174inputContainer.classList.toggle('focused', true);1175}));1176this._register(this._inputEditor.onDidBlurEditorText(() => {1177this.inputEditorHasFocus.set(false);1178inputContainer.classList.toggle('focused', false);11791180this._onDidBlur.fire();1181}));1182this._register(this._inputEditor.onDidBlurEditorWidget(() => {1183CopyPasteController.get(this._inputEditor)?.clearWidgets();1184DropIntoEditorController.get(this._inputEditor)?.clearWidgets();1185}));11861187const hoverDelegate = this._register(createInstantHoverDelegate());11881189this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));1190this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));1191this.inputActionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, MenuId.ChatInput, {1192telemetrySource: this.options.menus.telemetrySource,1193menuOptions: { shouldForwardArgs: true },1194hiddenItemStrategy: HiddenItemStrategy.Ignore,1195hoverDelegate,1196actionViewItemProvider: (action, options) => {1197if (action.id === ChatOpenModelPickerActionId && action instanceof MenuItemAction) {1198if (!this._currentLanguageModel) {1199this.setCurrentLanguageModelToDefault();1200}12011202const itemDelegate: IModelPickerDelegate = {1203getCurrentModel: () => this._currentLanguageModel,1204onDidChangeModel: this._onDidChangeCurrentLanguageModel.event,1205setModel: (model: ILanguageModelChatMetadataAndIdentifier) => {1206// The user changed the language model, so we don't wait for the persisted option to be registered1207this._waitForPersistedLanguageModel.clear();1208this.setCurrentLanguageModel(model);1209this.renderAttachedContext();1210},1211getModels: () => this.getModels()1212};1213return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, itemDelegate);1214} else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) {1215const delegate: IModePickerDelegate = {1216currentMode: this._currentModeObservable1217};1218return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate);1219}12201221return undefined;1222}1223}));1224this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar');1225this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext;1226this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => {1227if (this.cachedDimensions && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) {1228this.layout(this.cachedDimensions.height, this.cachedDimensions.width);1229}1230}));1231this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, {1232telemetrySource: this.options.menus.telemetrySource,1233menuOptions: {1234shouldForwardArgs: true1235},1236hoverDelegate,1237hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu1238actionViewItemProvider: (action, options) => {1239if (this.location === ChatAgentLocation.Panel || this.location === ChatAgentLocation.Editor) {1240if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID) && action instanceof MenuItemAction) {1241const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined);1242return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false });1243}1244}12451246return undefined;1247}1248}));1249this.executeToolbar.getElement().classList.add('chat-execute-toolbar');1250this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext;1251this._register(this.executeToolbar.onDidChangeMenuItems(() => {1252if (this.cachedDimensions && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) {1253this.layout(this.cachedDimensions.height, this.cachedDimensions.width);1254}1255}));1256if (this.options.menus.inputSideToolbar) {1257const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, this.options.menus.inputSideToolbar, {1258telemetrySource: this.options.menus.telemetrySource,1259menuOptions: {1260shouldForwardArgs: true1261},1262hoverDelegate1263}));1264this.inputSideToolbarContainer = toolbarSide.getElement();1265toolbarSide.getElement().classList.add('chat-side-toolbar');1266toolbarSide.context = { widget } satisfies IChatExecuteActionContext;1267}12681269let inputModel = this.modelService.getModel(this.inputUri);1270if (!inputModel) {1271inputModel = this.modelService.createModel('', null, this.inputUri, true);1272}12731274this.textModelResolverService.createModelReference(this.inputUri).then(ref => {1275// make sure to hold a reference so that the model doesn't get disposed by the text model service1276if (this._store.isDisposed) {1277ref.dispose();1278return;1279}1280this._register(ref);1281});12821283this.inputModel = inputModel;1284this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } });1285this._inputEditor.setModel(this.inputModel);1286if (initialValue) {1287this.inputModel.setValue(initialValue);1288const lineNumber = this.inputModel.getLineCount();1289this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) });1290}12911292const onDidChangeCursorPosition = () => {1293const model = this._inputEditor.getModel();1294if (!model) {1295return;1296}12971298const position = this._inputEditor.getPosition();1299if (!position) {1300return;1301}13021303const atTop = position.lineNumber === 1 && position.column - 1 <= (this._inputEditor._getViewModel()?.getLineLength(1) ?? 0);1304this.chatCursorAtTop.set(atTop);13051306this.historyNavigationBackwardsEnablement.set(atTop);1307this.historyNavigationForewardsEnablement.set(position.equals(getLastPosition(model)));1308};1309this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition()));1310onDidChangeCursorPosition();13111312this._register(this.themeService.onDidFileIconThemeChange(() => {1313this.renderAttachedContext();1314}));13151316this.addFilesToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, {1317telemetrySource: this.options.menus.telemetrySource,1318label: true,1319menuOptions: { shouldForwardArgs: true, renderShortTitle: true },1320hiddenItemStrategy: HiddenItemStrategy.Ignore,1321hoverDelegate,1322actionViewItemProvider: (action, options) => {1323if (action.id === 'workbench.action.chat.attachContext') {1324const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options);1325return viewItem;1326}1327return undefined;1328}1329}));1330this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') };1331this._register(this.addFilesToolbar.onDidChangeMenuItems(() => {1332if (this.cachedDimensions) {1333this._onDidChangeHeight.fire();1334}1335}));1336}13371338public toggleChatInputOverlay(editing: boolean): void {1339this.chatInputOverlay.classList.toggle('disabled', editing);1340if (editing) {1341this.overlayClickListener.value = dom.addStandardDisposableListener(this.chatInputOverlay, dom.EventType.CLICK, e => {1342e.preventDefault();1343e.stopPropagation();1344this._onDidClickOverlay.fire();1345});1346} else {1347this.overlayClickListener.clear();1348}1349}13501351public renderAttachedContext() {1352const container = this.attachedContextContainer;1353// Note- can't measure attachedContextContainer, because it has `display: contents`, so measure the parent to check for height changes1354const oldHeight = this.attachmentsContainer.offsetHeight;1355const store = new DisposableStore();1356this.attachedContextDisposables.value = store;13571358dom.clearNode(container);1359const hoverDelegate = store.add(createInstantHoverDelegate());13601361store.add(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {1362this.handleAttachmentNavigation(e);1363}));13641365const attachments = [...this.attachmentModel.attachments.entries()];1366const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value);1367dom.setVisibility(Boolean(hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer);1368dom.setVisibility(hasAttachments, this.attachedContextContainer);1369if (!attachments.length) {1370this._indexOfLastAttachedContextDeletedWithKeyboard = -1;1371this._indexOfLastOpenedContext = -1;1372}13731374const isSuggestedEnabled = this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext');13751376if (this.implicitContext?.value && !isSuggestedEnabled) {1377const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this.attachmentModel));1378container.appendChild(implicitPart.domNode);1379}13801381this.promptFileAttached.set(this.hasPromptFileAttachments);13821383for (const [index, attachment] of attachments) {1384const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;1385const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined;1386const shouldFocusClearButton = index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1) && this._indexOfLastAttachedContextDeletedWithKeyboard > -1;13871388let attachmentWidget;1389const options = { shouldFocusClearButton, supportsDeletion: true };1390if (attachment.kind === 'tool' || attachment.kind === 'toolset') {1391attachmentWidget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1392} else if (resource && isNotebookOutputVariableEntry(attachment)) {1393attachmentWidget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1394} else if (isPromptFileVariableEntry(attachment)) {1395attachmentWidget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1396} else if (isPromptTextVariableEntry(attachment)) {1397attachmentWidget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, options, container, this._contextResourceLabels, hoverDelegate);1398} else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) {1399attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1400} else if (isImageVariableEntry(attachment)) {1401attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1402} else if (isElementVariableEntry(attachment)) {1403attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1404} else if (isPasteVariableEntry(attachment)) {1405attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1406} else if (isSCMHistoryItemVariableEntry(attachment)) {1407attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1408} else {1409attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);1410}14111412if (shouldFocusClearButton) {1413attachmentWidget.element.focus();1414}14151416if (index === Math.min(this._indexOfLastOpenedContext, this.attachmentModel.size - 1)) {1417attachmentWidget.element.focus();1418}14191420store.add(attachmentWidget);1421store.add(attachmentWidget.onDidDelete(e => {1422this.handleAttachmentDeletion(e, index, attachment);1423}));14241425store.add(attachmentWidget.onDidOpen(e => {1426this.handleAttachmentOpen(index, attachment);1427}));1428}14291430const implicitUri = this.implicitContext?.value;1431const isUri = URI.isUri(implicitUri);14321433if (isSuggestedEnabled && implicitUri && (isUri || isLocation(implicitUri))) {1434const targetUri = isUri ? implicitUri : implicitUri.uri;1435const currentlyAttached = attachments.some(([, attachment]) => URI.isUri(attachment.value) && isEqual(attachment.value, targetUri));14361437const shouldShowImplicit = isUri ? !currentlyAttached : implicitUri.range;1438if (shouldShowImplicit) {1439const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this._attachmentModel));1440container.appendChild(implicitPart.domNode);1441}1442}14431444if (oldHeight !== this.attachmentsContainer.offsetHeight) {1445this._onDidChangeHeight.fire();1446}14471448this._indexOfLastOpenedContext = -1;1449}14501451private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) {1452// Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click)1453if (dom.isKeyboardEvent(e)) {1454this._indexOfLastAttachedContextDeletedWithKeyboard = index;1455}14561457this._attachmentModel.delete(attachment.id);145814591460if (this.configurationService.getValue<boolean>('chat.implicitContext.enableImplicitContext')) {1461// if currently opened file is deleted, do not show implicit context1462const implicitValue = URI.isUri(this.implicitContext?.value) && URI.isUri(attachment.value) && isEqual(this.implicitContext.value, attachment.value);14631464if (this.implicitContext?.isFile && implicitValue) {1465this.implicitContext.enabled = false;1466}1467}14681469if (this._attachmentModel.size === 0) {1470this.focus();1471}14721473this._onDidChangeContext.fire({ removed: [attachment] });1474this.renderAttachedContext();1475}14761477private handleAttachmentOpen(index: number, attachment: IChatRequestVariableEntry): void {1478this._indexOfLastOpenedContext = index;1479this._indexOfLastAttachedContextDeletedWithKeyboard = -1;14801481if (this._attachmentModel.size === 0) {1482this.focus();1483}1484}14851486private handleAttachmentNavigation(e: StandardKeyboardEvent): void {1487if (!e.equals(KeyCode.LeftArrow) && !e.equals(KeyCode.RightArrow)) {1488return;1489}14901491const toolbar = this.addFilesToolbar?.getElement().querySelector('.action-label');1492if (!toolbar) {1493return;1494}14951496const attachments = Array.from(this.attachedContextContainer.querySelectorAll('.chat-attached-context-attachment'));1497if (!attachments.length) {1498return;1499}15001501attachments.unshift(toolbar);15021503const activeElement = dom.getWindow(this.attachmentsContainer).document.activeElement;1504const currentIndex = attachments.findIndex(attachment => attachment === activeElement);1505let newIndex = currentIndex;15061507if (e.equals(KeyCode.LeftArrow)) {1508newIndex = currentIndex > 0 ? currentIndex - 1 : attachments.length - 1;1509} else if (e.equals(KeyCode.RightArrow)) {1510newIndex = currentIndex < attachments.length - 1 ? currentIndex + 1 : 0;1511}15121513if (newIndex !== -1) {1514const nextElement = attachments[newIndex] as HTMLElement;1515nextElement.focus();1516e.preventDefault();1517e.stopPropagation();1518}1519}15201521async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) {1522dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer);15231524if (chatEditingSession) {1525if (chatEditingSession.chatSessionId !== this._lastEditingSessionId) {1526this._workingSetCollapsed = true;1527}1528this._lastEditingSessionId = chatEditingSession.chatSessionId;1529}15301531const seenEntries = new ResourceSet();1532const entries: IChatCollapsibleListItem[] = [];1533if (chatEditingSession) {1534for (const entry of chatEditingSession.entries.get()) {1535if (entry.state.get() !== ModifiedFileEntryState.Modified) {1536continue;1537}15381539if (!seenEntries.has(entry.modifiedURI)) {1540seenEntries.add(entry.modifiedURI);1541const linesAdded = entry.linesAdded?.get();1542const linesRemoved = entry.linesRemoved?.get();1543entries.push({1544reference: entry.modifiedURI,1545state: entry.state.get(),1546kind: 'reference',1547options: {1548status: undefined,1549diffMeta: { added: linesAdded ?? 0, removed: linesRemoved ?? 0 }1550}1551});1552}1553}1554}15551556if (!chatEditingSession || !this.options.renderWorkingSet || !entries.length) {1557dom.clearNode(this.chatEditingSessionWidgetContainer);1558this._chatEditsDisposables.clear();1559this._chatEditList = undefined;1560return;1561}15621563// Summary of number of files changed1564const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons'));15651566entries.sort((a, b) => {1567if (a.kind === 'reference' && b.kind === 'reference') {1568if (a.state === b.state || a.state === undefined || b.state === undefined) {1569return a.reference.toString().localeCompare(b.reference.toString());1570}1571return a.state - b.state;1572}1573return 0;1574});15751576const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview'));1577const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title'));15781579// Clear out the previous actions (if any)1580this._chatEditsActionsDisposables.clear();15811582// Chat editing session actions1583const actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement ?? dom.append(overviewRegion, $('.chat-editing-session-actions'));15841585this._chatEditsActionsDisposables.add(this.instantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, MenuId.ChatEditingWidgetToolbar, {1586telemetrySource: this.options.menus.telemetrySource,1587menuOptions: {1588arg: { sessionId: chatEditingSession.chatSessionId },1589},1590buttonConfigProvider: (action) => {1591if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id) {1592return { showIcon: true, showLabel: false, isSecondary: true };1593}1594return undefined;1595}1596}));15971598if (!chatEditingSession) {1599return;1600}16011602// Working set1603const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list'));16041605const button = this._chatEditsActionsDisposables.add(new ButtonWithIcon(overviewTitle, {1606supportIcons: true,1607secondary: true,1608ariaLabel: localize('chatEditingSession.toggleWorkingSet', 'Toggle changed files.'),1609}));16101611let added = 0;1612let removed = 0;1613if (chatEditingSession) {1614for (const entry of chatEditingSession.entries.get()) {1615if (entry.linesAdded && entry.linesRemoved) {1616added += entry.linesAdded.get();1617removed += entry.linesRemoved.get();1618}1619}1620}16211622const baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length);1623button.label = baseLabel;16241625if (!this._workingSetLinesAddedSpan) {1626this._workingSetLinesAddedSpan = dom.$('.working-set-lines-added');1627}1628if (!this._workingSetLinesRemovedSpan) {1629this._workingSetLinesRemovedSpan = dom.$('.working-set-lines-removed');1630}16311632const countsContainer = dom.$('.working-set-line-counts');1633button.element.appendChild(countsContainer);1634countsContainer.appendChild(this._workingSetLinesAddedSpan);1635countsContainer.appendChild(this._workingSetLinesRemovedSpan);16361637this._workingSetLinesAddedSpan.textContent = `+${added}`;1638this._workingSetLinesRemovedSpan.textContent = `-${removed}`;1639button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', baseLabel, added, removed));16401641const applyCollapseState = () => {1642button.icon = this._workingSetCollapsed ? Codicon.chevronRight : Codicon.chevronDown;1643workingSetContainer.classList.toggle('collapsed', this._workingSetCollapsed);1644this._onDidChangeHeight.fire();1645};16461647const toggleWorkingSet = () => {1648this._workingSetCollapsed = !this._workingSetCollapsed;1649applyCollapseState();1650};16511652this._chatEditsActionsDisposables.add(button.onDidClick(() => { toggleWorkingSet(); }));1653this._chatEditsActionsDisposables.add(addDisposableListener(overviewRegion, 'click', e => {1654if (e.defaultPrevented) {1655return;1656}1657const target = e.target as HTMLElement;1658if (target.closest('.monaco-button')) {1659return;1660}1661toggleWorkingSet();1662}));16631664applyCollapseState();16651666if (!this._chatEditList) {1667this._chatEditList = this._chatEditsListPool.get();1668const list = this._chatEditList.object;1669this._chatEditsDisposables.add(this._chatEditList);1670this._chatEditsDisposables.add(list.onDidFocus(() => {1671this._onDidFocus.fire();1672}));1673this._chatEditsDisposables.add(list.onDidOpen(async (e) => {1674if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) {1675const modifiedFileUri = e.element.reference;16761677const entry = chatEditingSession.getEntry(modifiedFileUri);16781679const pane = await this.editorService.openEditor({1680resource: modifiedFileUri,1681options: e.editorOptions1682}, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP);16831684if (pane) {1685entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus);1686}1687}1688}));1689this._chatEditsDisposables.add(addDisposableListener(list.getHTMLElement(), 'click', e => {1690if (!this.hasFocus()) {1691this._onDidFocus.fire();1692}1693}, true));1694dom.append(workingSetContainer, list.getHTMLElement());1695dom.append(innerContainer, workingSetContainer);1696}16971698const maxItemsShown = 6;1699const itemsShown = Math.min(entries.length, maxItemsShown);1700const height = itemsShown * 22;1701const list = this._chatEditList.object;1702list.layout(height);1703list.getHTMLElement().style.height = `${height}px`;1704list.splice(0, list.length, entries);1705this._onDidChangeHeight.fire();1706}17071708async renderChatRelatedFiles() {1709const anchor = this.relatedFilesContainer;1710dom.clearNode(anchor);1711const shouldRender = this.configurationService.getValue('chat.renderRelatedFiles');1712dom.setVisibility(Boolean(this.relatedFiles?.value.length && shouldRender), anchor);1713if (!shouldRender || !this.relatedFiles?.value.length) {1714return;1715}17161717const hoverDelegate = getDefaultHoverDelegate('element');1718for (const { uri, description } of this.relatedFiles.value) {1719const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, {1720supportIcons: true,1721secondary: true,1722hoverDelegate1723}));1724uriLabel.label = this.labelService.getUriBasenameLabel(uri);1725uriLabel.element.classList.add('monaco-icon-label');1726uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), description ?? '');17271728this._chatEditsActionsDisposables.add(uriLabel.onDidClick(async () => {1729group.remove(); // REMOVE asap1730await this._attachmentModel.addFile(uri);1731this.relatedFiles?.remove(uri);1732}));17331734const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, {1735supportIcons: false,1736secondary: true,1737hoverDelegate,1738ariaLabel: localize('chatEditingSession.addSuggestion', 'Add suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })),1739}));1740addButton.icon = Codicon.add;1741addButton.setTitle(localize('chatEditingSession.addSuggested', 'Add suggestion'));1742this._chatEditsActionsDisposables.add(addButton.onDidClick(async () => {1743group.remove(); // REMOVE asap1744await this._attachmentModel.addFile(uri);1745this.relatedFiles?.remove(uri);1746}));17471748const sep = document.createElement('div');1749sep.classList.add('separator');17501751const group = document.createElement('span');1752group.classList.add('monaco-button-dropdown', 'sidebyside-button');1753group.appendChild(addButton.element);1754group.appendChild(sep);1755group.appendChild(uriLabel.element);1756dom.append(anchor, group);17571758this._chatEditsActionsDisposables.add(toDisposable(() => {1759group.remove();1760}));1761}1762this._onDidChangeHeight.fire();1763}17641765async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise<void> {1766if (!this.options.renderFollowups) {1767return;1768}1769this.followupsDisposables.clear();1770dom.clearNode(this.followupsContainer);17711772if (items && items.length > 0) {1773this.followupsDisposables.add(this.instantiationService.createInstance<typeof ChatFollowups<IChatFollowup>, ChatFollowups<IChatFollowup>>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response })));1774}1775this._onDidChangeHeight.fire();1776}17771778get contentHeight(): number {1779const data = this.getLayoutData();1780return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight;1781}17821783layout(height: number, width: number) {1784this.cachedDimensions = new dom.Dimension(width, height);17851786return this._layout(height, width);1787}17881789private previousInputEditorDimension: IDimension | undefined;1790private _layout(height: number, width: number, allowRecurse = true): void {1791const data = this.getLayoutData();1792const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.attachmentsHeight - data.inputPartVerticalPadding - data.toolbarsHeight);17931794const followupsWidth = width - data.inputPartHorizontalPadding;1795this.followupsContainer.style.width = `${followupsWidth}px`;17961797this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight;1798this._followupsHeight = data.followupsHeight;1799this._editSessionWidgetHeight = data.chatEditingStateHeight;18001801const initialEditorScrollWidth = this._inputEditor.getScrollWidth();1802const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth;1803const newDimension = { width: newEditorWidth, height: inputEditorHeight };1804if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) {1805// This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler1806// to be invoked, and we have a lot of these on this editor. Only doing a layout this when the editor size has actually changed makes it much easier to follow.1807this._inputEditor.layout(newDimension);1808this.previousInputEditorDimension = newDimension;1809}18101811if (allowRecurse && initialEditorScrollWidth < 10) {1812// This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight1813return this._layout(height, width, false);1814}1815}18161817private getLayoutData() {1818const executeToolbarWidth = this.cachedExecuteToolbarWidth = this.executeToolbar.getItemsWidth();1819const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth();1820const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * 4;1821const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * 4 : 0;1822return {1823inputEditorBorder: 2,1824followupsHeight: this.followupsContainer.offsetHeight,1825inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight),1826inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32,1827inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28,1828attachmentsHeight: this.attachmentsHeight,1829editorBorder: 2,1830inputPartHorizontalPaddingInside: 12,1831toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0,1832toolbarsHeight: this.options.renderStyle === 'compact' ? 0 : 22,1833chatEditingStateHeight: this.chatEditingSessionWidgetContainer.offsetHeight,1834sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0,1835};1836}18371838getViewState(): IChatInputState {1839return this.getInputState();1840}18411842saveState(): void {1843if (this.history.isAtEnd()) {1844this.saveCurrentValue(this.getInputState());1845}18461847const inputHistory = [...this.history];1848this.historyService.saveHistory(this.location, inputHistory);1849}1850}18511852const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify({ ...entry, state: { ...entry.state, chatMode: undefined } });18531854function getLastPosition(model: ITextModel): IPosition {1855return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 };1856}18571858// This does seems like a lot just to customize an item with dropdown. This whole class exists just because we need an1859// onDidChange listener on the submenu, which is apparently not needed in other cases.1860class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem {1861constructor(1862action: MenuItemAction,1863dropdownAction: IAction,1864options: IDropdownWithPrimaryActionViewItemOptions,1865@IMenuService menuService: IMenuService,1866@IContextMenuService contextMenuService: IContextMenuService,1867@IContextKeyService contextKeyService: IContextKeyService,1868@IKeybindingService keybindingService: IKeybindingService,1869@INotificationService notificationService: INotificationService,1870@IThemeService themeService: IThemeService,1871@IAccessibilityService accessibilityService: IAccessibilityService1872) {1873super(1874action,1875dropdownAction,1876[],1877'',1878{1879...options,1880getKeyBinding: (action: IAction) => keybindingService.lookupKeybinding(action.id, contextKeyService)1881},1882contextMenuService,1883keybindingService,1884notificationService,1885contextKeyService,1886themeService,1887accessibilityService);1888const menu = menuService.createMenu(MenuId.ChatExecuteSecondary, contextKeyService);1889const setActions = () => {1890const secondary = getFlatActionBarActions(menu.getActions({ shouldForwardArgs: true }));1891this.update(dropdownAction, secondary);1892};1893setActions();1894this._register(menu.onDidChange(() => setActions()));1895}1896}18971898const chatInputEditorContainerSelector = '.interactive-input-editor';1899setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector);19001901class AddFilesButton extends ActionViewItem {19021903constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {1904super(context, action, {1905...options,1906icon: false,1907label: true,1908keybindingNotRenderedWithLabel: true,1909});1910}19111912override render(container: HTMLElement): void {1913container.classList.add('chat-attachment-button');1914super.render(container);1915}19161917protected override updateLabel(): void {1918assertType(this.label);1919const message = `$(attach) ${this.action.label}`;1920dom.reset(this.label, ...renderLabelWithIcons(message));1921}1922}192319241925