Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts
5236 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 { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';6import { alert } from '../../../../base/browser/ui/aria/aria.js';7import { raceCancellation } from '../../../../base/common/async.js';8import { CancellationToken } from '../../../../base/common/cancellation.js';9import { onUnexpectedError } from '../../../../base/common/errors.js';10import { Event } from '../../../../base/common/event.js';11import { Lazy } from '../../../../base/common/lazy.js';12import { DisposableStore } from '../../../../base/common/lifecycle.js';13import { Schemas } from '../../../../base/common/network.js';14import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js';15import { isEqual } from '../../../../base/common/resources.js';16import { assertType } from '../../../../base/common/types.js';17import { URI } from '../../../../base/common/uri.js';18import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';19import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';20import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';21import { IPosition, Position } from '../../../../editor/common/core/position.js';22import { IRange, Range } from '../../../../editor/common/core/range.js';23import { ISelection, Selection } from '../../../../editor/common/core/selection.js';24import { IEditorContribution } from '../../../../editor/common/editorCommon.js';25import { TextEdit } from '../../../../editor/common/languages.js';26import { ITextModel } from '../../../../editor/common/model.js';27import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js';28import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js';29import { localize } from '../../../../nls.js';30import { MenuId } from '../../../../platform/actions/common/actions.js';31import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';32import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';33import { IFileService } from '../../../../platform/files/common/files.js';34import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';35import { ILogService } from '../../../../platform/log/common/log.js';36import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';37import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';38import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';39import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js';40import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js';41import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js';42import { ChatModel } from '../../chat/common/model/chatModel.js';43import { ChatMode } from '../../chat/common/chatModes.js';44import { IChatService } from '../../chat/common/chatService/chatService.js';45import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js';46import { isResponseVM } from '../../chat/common/model/chatViewModel.js';47import { ChatAgentLocation } from '../../chat/common/constants.js';48import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js';49import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js';50import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';51import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js';52import { INotebookService } from '../../notebook/common/notebookService.js';53import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';54import { InlineChatAffordance } from './inlineChatAffordance.js';55import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js';56import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js';57import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';58import { InlineChatZoneWidget } from './inlineChatZoneWidget.js';596061export abstract class InlineChatRunOptions {6263initialSelection?: ISelection;64initialRange?: IRange;65message?: string;66attachments?: URI[];67autoSend?: boolean;68position?: IPosition;69modelSelector?: ILanguageModelChatSelector;70resolveOnResponse?: boolean;7172static isInlineChatRunOptions(options: unknown): options is InlineChatRunOptions {7374if (typeof options !== 'object' || options === null) {75return false;76}7778const { initialSelection, initialRange, message, autoSend, position, attachments, modelSelector, resolveOnResponse } = <InlineChatRunOptions>options;79if (80typeof message !== 'undefined' && typeof message !== 'string'81|| typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean'82|| typeof initialRange !== 'undefined' && !Range.isIRange(initialRange)83|| typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection)84|| typeof position !== 'undefined' && !Position.isIPosition(position)85|| typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI))86|| typeof modelSelector !== 'undefined' && !isILanguageModelChatSelector(modelSelector)87|| typeof resolveOnResponse !== 'undefined' && typeof resolveOnResponse !== 'boolean'88) {89return false;90}9192return true;93}94}9596// TODO@jrieken THIS should be shared with the code in MainThreadEditors97function getEditorId(editor: ICodeEditor, model: ITextModel): string {98return `${editor.getId()},${model.id}`;99}100101export class InlineChatController implements IEditorContribution {102103static readonly ID = 'editor.contrib.inlineChatController';104105static get(editor: ICodeEditor): InlineChatController | undefined {106return editor.getContribution<InlineChatController>(InlineChatController.ID) ?? undefined;107}108109/**110* Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session.111* When set, this takes priority over the inlineChat.defaultModel setting.112*/113private static _userSelectedModel: string | undefined;114115private readonly _store = new DisposableStore();116private readonly _isActiveController = observableValue(this, false);117private readonly _renderMode: IObservable<'zone' | 'hover'>;118private readonly _zone: Lazy<InlineChatZoneWidget>;119private readonly _gutterIndicator: InlineChatAffordance;120121private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;122123get widget(): EditorBasedInlineChatWidget {124return this._zone.value.widget;125}126127get isActive() {128return Boolean(this._currentSession.get());129}130131constructor(132private readonly _editor: ICodeEditor,133@IInstantiationService private readonly _instaService: IInstantiationService,134@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,135@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,136@ICodeEditorService codeEditorService: ICodeEditorService,137@IContextKeyService contextKeyService: IContextKeyService,138@IConfigurationService private readonly _configurationService: IConfigurationService,139@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,140@IFileService private readonly _fileService: IFileService,141@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,142@IEditorService private readonly _editorService: IEditorService,143@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,144@ILanguageModelsService private readonly _languageModelService: ILanguageModelsService,145@ILogService private readonly _logService: ILogService,146) {147const editorObs = observableCodeEditor(_editor);148149150const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);151const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService);152this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService);153154const overlayWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs));155const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));156this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget));157158this._zone = new Lazy<InlineChatZoneWidget>(() => {159160assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');161162const location: IChatWidgetLocationOptions = {163location: ChatAgentLocation.EditorInline,164resolveData: () => {165assertType(this._editor.hasModel());166const wholeRange = this._editor.getSelection();167const document = this._editor.getModel().uri;168169return {170type: ChatAgentLocation.EditorInline,171id: getEditorId(this._editor, this._editor.getModel()),172selection: this._editor.getSelection(),173document,174wholeRange175};176}177};178179// inline chat in notebooks180// check if this editor is part of a notebook editor181// if so, update the location and use the notebook specific widget182const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor);183if (!!notebookEditor) {184location.location = ChatAgentLocation.Notebook;185if (notebookAgentConfig.get()) {186location.resolveData = () => {187assertType(this._editor.hasModel());188189return {190type: ChatAgentLocation.Notebook,191sessionInputUri: this._editor.getModel().uri,192};193};194}195}196197const result = this._instaService.createInstance(InlineChatZoneWidget,198location,199{200enableWorkingSet: 'implicit',201enableImplicitContext: false,202renderInputOnTop: false,203renderInputToolbarBelowInput: true,204filter: item => {205if (!isResponseVM(item)) {206return false;207}208return !!item.model.isPendingConfirmation.get();209},210menus: {211telemetrySource: 'inlineChatWidget',212executeToolbar: MenuId.ChatEditorInlineExecute,213inputSideToolbar: MenuId.ChatEditorInlineInputSide214},215defaultMode: ChatMode.Ask216},217{ editor: this._editor, notebookEditor },218() => Promise.resolve(),219);220221this._store.add(result);222223result.domNode.classList.add('inline-chat-2');224225return result;226});227228229230const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions);231232this._currentSession = derived(r => {233sessionsSignal.read(r);234const model = editorObs.model.read(r);235const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri);236return session ?? undefined;237});238239240let lastSession: IInlineChatSession2 | undefined = undefined;241242this._store.add(autorun(r => {243const session = this._currentSession.read(r);244if (!session) {245this._isActiveController.set(false, undefined);246247if (lastSession && !lastSession.chatModel.hasRequests) {248const state = lastSession.chatModel.inputModel.state.read(undefined);249if (!state || (!state.inputText && state.attachments.length === 0)) {250lastSession.dispose();251lastSession = undefined;252}253}254return;255}256257lastSession = session;258259let foundOne = false;260for (const editor of codeEditorService.listCodeEditors()) {261if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) {262foundOne = true;263break;264}265}266if (!foundOne && editorObs.isFocused.read(r)) {267this._isActiveController.set(true, undefined);268}269}));270271const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);272273this._store.add(autorun(r => {274275const model = editorObs.model.read(r);276const session = this._currentSession.read(r);277const isActive = this._isActiveController.read(r);278279if (!session || !isActive || !model) {280visibleSessionObs.set(undefined, undefined);281} else {282visibleSessionObs.set(session, undefined);283}284}));285286const defaultPlaceholderObs = visibleSessionObs.map((session, r) => {287return session?.initialSelection.isEmpty()288? localize('placeholder', "Generate code")289: localize('placeholderWithSelection', "Modify selected code");290});291292293this._store.add(autorun(r => {294295// HIDE/SHOW296const session = visibleSessionObs.read(r);297const renderMode = this._renderMode.read(r);298if (!session) {299this._zone.rawValue?.hide();300this._zone.rawValue?.widget.chatWidget.setModel(undefined);301_editor.focus();302ctxInlineChatVisible.reset();303} else if (renderMode === 'hover') {304// hover mode: set model but don't show zone, keep focus in editor305this._zone.value.widget.chatWidget.setModel(session.chatModel);306this._zone.rawValue?.hide();307ctxInlineChatVisible.set(true);308} else {309ctxInlineChatVisible.set(true);310this._zone.value.widget.chatWidget.setModel(session.chatModel);311if (!this._zone.value.position) {312this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));313this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug314this._zone.value.show(session.initialPosition);315}316this._zone.value.reveal(this._zone.value.position!);317this._zone.value.widget.focus();318}319}));320321// Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled322this._store.add(autorun(r => {323const session = visibleSessionObs.read(r);324const renderMode = this._renderMode.read(r);325if (!session || renderMode !== 'hover') {326sessionOverlayWidget.hide();327return;328}329const lastRequest = session.chatModel.lastRequestObs.read(r);330const isInProgress = lastRequest?.response?.isInProgress.read(r);331const entry = session.editingSession.readEntry(session.uri, r);332// When there's no entry (no changes made) and the response is complete, the widget should be hidden.333// When there's an entry in Modified state, it needs to be settled (accepted/rejected).334const isNotSettled = entry ? entry.state.read(r) === ModifiedFileEntryState.Modified : false;335if (isInProgress || isNotSettled) {336sessionOverlayWidget.show(session);337} else {338sessionOverlayWidget.hide();339}340}));341342this._store.add(autorun(r => {343const session = visibleSessionObs.read(r);344if (session) {345const entries = session.editingSession.entries.read(r);346const sessionCellUri = CellUri.parse(session.uri);347const otherEntries = entries.filter(entry => {348if (isEqual(entry.modifiedURI, session.uri)) {349return false;350}351// Don't count notebooks that include the session's cell352if (!!sessionCellUri && isEqual(sessionCellUri.notebook, entry.modifiedURI)) {353return false;354}355return true;356});357for (const entry of otherEntries) {358// OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend359// that modifies other files360this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);361}362}363}));364365const lastResponseObs = visibleSessionObs.map((session, r) => {366if (!session) {367return;368}369const lastRequest = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().at(-1)).read(r);370return lastRequest?.response;371});372373const lastResponseProgressObs = lastResponseObs.map((response, r) => {374if (!response) {375return;376}377return observableFromEvent(this, response.onDidChange, () => response.response.value.findLast(part => part.kind === 'progressMessage')).read(r);378});379380381this._store.add(autorun(r => {382const response = lastResponseObs.read(r);383384this._zone.rawValue?.widget.updateInfo('');385386if (!response?.isInProgress.read(r)) {387388if (response?.result?.errorDetails) {389// ERROR case390this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);391alert(response.result.errorDetails.message);392}393394// no response or not in progress395this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);396this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));397398} else {399this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);400let placeholder = response.request?.message.text;401const lastProgress = lastResponseProgressObs.read(r);402if (lastProgress) {403placeholder = renderAsPlaintext(lastProgress.content);404}405this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));406}407408}));409410this._store.add(autorun(r => {411const session = visibleSessionObs.read(r);412if (!session) {413return;414}415416const entry = session.editingSession.readEntry(session.uri, r);417if (entry?.state.read(r) === ModifiedFileEntryState.Modified) {418entry?.enableReviewModeUntilSettled();419}420}));421422423this._store.add(autorun(r => {424425const session = visibleSessionObs.read(r);426const entry = session?.editingSession.readEntry(session.uri, r);427428// make sure there is an editor integration429const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));430if (pane && entry) {431entry?.getEditorIntegration(pane);432}433434// make sure the ZONE isn't inbetween a diff and move above if so435if (entry?.diffInfo && this._zone.value.position) {436const { position } = this._zone.value;437const diff = entry.diffInfo.read(r);438439for (const change of diff.changes) {440if (change.modified.contains(position.lineNumber)) {441this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));442break;443}444}445}446}));447}448449dispose(): void {450this._store.dispose();451}452453getWidgetPosition(): Position | undefined {454return this._zone.rawValue?.position;455}456457focus() {458this._zone.rawValue?.widget.focus();459}460461async run(arg?: InlineChatRunOptions): Promise<boolean> {462assertType(this._editor.hasModel());463const uri = this._editor.getModel().uri;464465const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri);466if (existingSession) {467await existingSession.editingSession.accept();468existingSession.dispose();469}470471// use hover overlay to ask for input472if (!arg?.message && this._configurationService.getValue<string>(InlineChatConfigKeys.RenderMode) === 'hover') {473// show menu and RETURN because the menu is re-entrant474await this._gutterIndicator.showMenuAtSelection();475return true;476}477478this._isActiveController.set(true, undefined);479480const session = this._inlineChatSessionService.createSession(this._editor);481482483// Store for tracking model changes during this session484const sessionStore = new DisposableStore();485486try {487await this._applyModelDefaults(session, sessionStore);488489// ADD diagnostics490const entries: IChatRequestVariableEntry[] = [];491for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) {492if (range.intersectRanges(this._editor.getSelection())) {493const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);494entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter));495}496}497if (entries.length > 0) {498this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries);499this._zone.value.widget.chatWidget.input.setValue(entries.length > 1500? localize('fixN', "Fix the attached problems")501: localize('fix1', "Fix the attached problem"),502true503);504this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));505}506507// Check args508if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {509if (arg.initialRange) {510this._editor.revealRange(arg.initialRange);511}512if (arg.initialSelection) {513this._editor.setSelection(arg.initialSelection);514}515if (arg.attachments) {516await Promise.all(arg.attachments.map(async attachment => {517await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);518}));519delete arg.attachments;520}521if (arg.modelSelector) {522const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);523if (!id) {524throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`);525}526const model = this._languageModelService.lookupLanguageModel(id);527if (!model) {528throw new Error(`Language model not loaded: ${id}.`);529}530this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });531}532if (arg.message) {533this._zone.value.widget.chatWidget.setInput(arg.message);534if (arg.autoSend) {535await this._zone.value.widget.chatWidget.acceptInput();536}537}538}539540if (!arg?.resolveOnResponse) {541// DEFAULT: wait for the session to be accepted or rejected542await Event.toPromise(session.editingSession.onDidDispose);543const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected;544return !rejected;545546} else {547// resolveOnResponse: ONLY wait for the file to be modified548const modifiedObs = derived(r => {549const entry = session.editingSession.readEntry(uri, r);550return entry?.state.read(r) === ModifiedFileEntryState.Modified && !entry?.isCurrentlyBeingModifiedBy.read(r);551});552await waitForState(modifiedObs, state => state === true);553return true;554}555} finally {556sessionStore.dispose();557}558}559560async acceptSession() {561const session = this._currentSession.get();562if (!session) {563return;564}565await session.editingSession.accept();566session.dispose();567}568569async rejectSession() {570const session = this._currentSession.get();571if (!session) {572return;573}574await session.editingSession.reject();575session.dispose();576}577578private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {579const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get();580if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) {581const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });582for (const identifier of ids) {583const candidate = this._languageModelService.lookupLanguageModel(identifier);584if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) {585this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });586break;587}588}589}590}591592/**593* Applies model defaults based on settings and tracks user model changes.594* Prioritization: user session choice > inlineChat.defaultModel setting > vendor default595*/596private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {597const userSelectedModel = InlineChatController._userSelectedModel;598const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);599600let modelApplied = false;601602// 1. Try user's explicitly chosen model from a previous inline chat in the same session603if (userSelectedModel) {604modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);605if (!modelApplied) {606// User's previously selected model is no longer available, clear it607InlineChatController._userSelectedModel = undefined;608}609}610611// 2. Try inlineChat.defaultModel setting612if (!modelApplied && defaultModelSetting) {613modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);614if (!modelApplied) {615this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);616}617}618619// 3. Fall back to vendor default620if (!modelApplied) {621await this._selectVendorDefaultModel(session);622}623624// Track model changes - store user's explicit choice in the given sessions.625// NOTE: This currently detects any model change, not just user-initiated ones.626let initialModelId: string | undefined;627sessionStore.add(autorun(r => {628const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);629if (!newModel) {630return;631}632if (!initialModelId) {633initialModelId = newModel.identifier;634return;635}636if (initialModelId !== newModel.identifier) {637// User explicitly changed model, store their choice as qualified name638InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);639initialModelId = newModel.identifier;640}641}));642}643644async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {645const value = this._currentSession.get();646if (!value) {647return undefined;648}649if (attachment.scheme === Schemas.file) {650if (await this._fileService.canHandleResource(attachment)) {651return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);652}653} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {654const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);655if (extractedImages) {656return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);657}658}659return undefined;660}661}662663export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable<TextEdit[]>, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {664if (!editor.hasModel()) {665return false;666}667668const chatService = accessor.get(IChatService);669const uri = editor.getModel().uri;670const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline);671const chatModel = chatModelRef.object as ChatModel;672673chatModel.startEditingSession(true);674675const store = new DisposableStore();676store.add(chatModelRef);677678// STREAM679const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, {680kind: undefined,681modeId: 'applyCodeBlock',682modeInstructions: undefined,683isBuiltin: true,684applyCodeBlockSuggestionId,685});686assertType(chatRequest.response);687chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });688for await (const chunk of stream) {689690if (token.isCancellationRequested) {691chatRequest.response.cancel();692break;693}694695chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false });696}697chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });698699if (!token.isCancellationRequested) {700chatRequest.response.complete();701}702703const isSettled = derived(r => {704const entry = chatModel.editingSession?.readEntry(uri, r);705if (!entry) {706return false;707}708const state = entry.state.read(r);709return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;710});711const whenDecided = waitForState(isSettled, Boolean);712await raceCancellation(whenDecided, token);713store.dispose();714return true;715}716717export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise<boolean> {718719const chatService = accessor.get(IChatService);720const notebookService = accessor.get(INotebookService);721const isNotebook = notebookService.hasSupportedNotebooks(uri);722const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline);723const chatModel = chatModelRef.object as ChatModel;724725chatModel.startEditingSession(true);726727const store = new DisposableStore();728store.add(chatModelRef);729730// STREAM731const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0);732assertType(chatRequest.response);733if (isNotebook) {734chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false });735} else {736chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });737}738for await (const chunk of stream) {739740if (token.isCancellationRequested) {741chatRequest.response.cancel();742break;743}744if (chunk.every(isCellEditOperation)) {745chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false });746} else {747chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false });748}749}750if (isNotebook) {751chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true });752} else {753chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });754}755756if (!token.isCancellationRequested) {757chatRequest.response.complete();758}759760const isSettled = derived(r => {761const entry = chatModel.editingSession?.readEntry(uri, r);762if (!entry) {763return false;764}765const state = entry.state.read(r);766return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;767});768769const whenDecided = waitForState(isSettled, Boolean);770771await raceCancellation(whenDecided, token);772773store.dispose();774775return true;776}777778function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation {779if (URI.isUri(edit)) {780return false;781}782if (Array.isArray(edit)) {783return false;784}785return true;786}787788789