Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.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 aria from '../../../../base/browser/ui/aria/aria.js';6import { Barrier, DeferredPromise, Queue, raceCancellation } from '../../../../base/common/async.js';7import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { toErrorMessage } from '../../../../base/common/errorMessage.js';9import { onUnexpectedError } from '../../../../base/common/errors.js';10import { Emitter, Event } from '../../../../base/common/event.js';11import { Lazy } from '../../../../base/common/lazy.js';12import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';13import { Schemas } from '../../../../base/common/network.js';14import { MovingAverage } from '../../../../base/common/numbers.js';15import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js';16import { isEqual } from '../../../../base/common/resources.js';17import { StopWatch } from '../../../../base/common/stopwatch.js';18import { assertType } from '../../../../base/common/types.js';19import { URI } from '../../../../base/common/uri.js';20import { generateUuid } from '../../../../base/common/uuid.js';21import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js';22import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';23import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';24import { EditorOption } from '../../../../editor/common/config/editorOptions.js';25import { IPosition, Position } from '../../../../editor/common/core/position.js';26import { IRange, Range } from '../../../../editor/common/core/range.js';27import { ISelection, Selection, SelectionDirection } from '../../../../editor/common/core/selection.js';28import { IEditorContribution } from '../../../../editor/common/editorCommon.js';29import { TextEdit, VersionedExtensionId } from '../../../../editor/common/languages.js';30import { IValidEditOperation } from '../../../../editor/common/model.js';31import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';32import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js';33import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js';34import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js';35import { localize } from '../../../../nls.js';36import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';37import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';38import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';39import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';40import { ILogService } from '../../../../platform/log/common/log.js';41import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';42import { IViewsService } from '../../../services/views/common/viewsService.js';43import { showChatView } from '../../chat/browser/chat.js';44import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';45import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js';46import { IChatRequestVariableEntry } from '../../chat/common/chatVariableEntries.js';47import { IChatService } from '../../chat/common/chatService.js';48import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';49import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js';50import { HunkInformation, Session, StashedSession } from './inlineChatSession.js';51import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js';52import { InlineChatError } from './inlineChatSessionServiceImpl.js';53import { HunkAction, IEditObserver, IInlineChatMetadata, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js';54import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';55import { InlineChatZoneWidget } from './inlineChatZoneWidget.js';56import { ChatAgentLocation } from '../../chat/common/constants.js';57import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';58import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js';59import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';60import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';61import { IFileService } from '../../../../platform/files/common/files.js';62import { IChatAttachmentResolveService } from '../../chat/browser/chatAttachmentResolveService.js';63import { INotebookService } from '../../notebook/common/notebookService.js';64import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';65import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js';66import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js';67import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js';6869export const enum State {70CREATE_SESSION = 'CREATE_SESSION',71INIT_UI = 'INIT_UI',72WAIT_FOR_INPUT = 'WAIT_FOR_INPUT',73SHOW_REQUEST = 'SHOW_REQUEST',74PAUSE = 'PAUSE',75CANCEL = 'CANCEL',76ACCEPT = 'DONE',77}7879const enum Message {80NONE = 0,81ACCEPT_SESSION = 1 << 0,82CANCEL_SESSION = 1 << 1,83PAUSE_SESSION = 1 << 2,84CANCEL_REQUEST = 1 << 3,85CANCEL_INPUT = 1 << 4,86ACCEPT_INPUT = 1 << 5,87}8889export abstract class InlineChatRunOptions {90initialSelection?: ISelection;91initialRange?: IRange;92message?: string;93attachments?: URI[];94autoSend?: boolean;95existingSession?: Session;96position?: IPosition;9798static isInlineChatRunOptions(options: any): options is InlineChatRunOptions {99const { initialSelection, initialRange, message, autoSend, position, existingSession, attachments: attachments } = <InlineChatRunOptions>options;100if (101typeof message !== 'undefined' && typeof message !== 'string'102|| typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean'103|| typeof initialRange !== 'undefined' && !Range.isIRange(initialRange)104|| typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection)105|| typeof position !== 'undefined' && !Position.isIPosition(position)106|| typeof existingSession !== 'undefined' && !(existingSession instanceof Session)107|| typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI))108) {109return false;110}111return true;112}113}114115export class InlineChatController implements IEditorContribution {116117static ID = 'editor.contrib.inlineChatController';118119static get(editor: ICodeEditor) {120return editor.getContribution<InlineChatController>(InlineChatController.ID);121}122123private readonly _delegate: IObservable<InlineChatController1 | InlineChatController2>;124125constructor(126editor: ICodeEditor,127@IConfigurationService configurationService: IConfigurationService,128) {129130const inlineChat2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configurationService);131132this._delegate = derived(r => {133if (inlineChat2.read(r)) {134return InlineChatController2.get(editor)!;135} else {136return InlineChatController1.get(editor)!;137}138});139}140141dispose(): void {142143}144145get isActive(): boolean {146return this._delegate.get().isActive;147}148149async run(arg?: InlineChatRunOptions): Promise<boolean> {150return this._delegate.get().run(arg);151}152153focus() {154return this._delegate.get().focus();155}156157get widget(): EditorBasedInlineChatWidget {158return this._delegate.get().widget;159}160161getWidgetPosition() {162return this._delegate.get().getWidgetPosition();163}164165acceptSession() {166return this._delegate.get().acceptSession();167}168}169170/**171* @deprecated172*/173export class InlineChatController1 implements IEditorContribution {174175static get(editor: ICodeEditor) {176return editor.getContribution<InlineChatController1>(INLINE_CHAT_ID);177}178179private _isDisposed: boolean = false;180private readonly _store = new DisposableStore();181182private readonly _ui: Lazy<InlineChatZoneWidget>;183184private readonly _ctxVisible: IContextKey<boolean>;185private readonly _ctxEditing: IContextKey<boolean>;186private readonly _ctxResponseType: IContextKey<undefined | InlineChatResponseType>;187private readonly _ctxRequestInProgress: IContextKey<boolean>;188189private readonly _ctxResponse: IContextKey<boolean>;190191private readonly _messages = this._store.add(new Emitter<Message>());192protected readonly _onDidEnterState = this._store.add(new Emitter<State>());193194get chatWidget() {195return this._ui.value.widget.chatWidget;196}197198private readonly _sessionStore = this._store.add(new DisposableStore());199private readonly _stashedSession = this._store.add(new MutableDisposable<StashedSession>());200private _session?: Session;201private _strategy?: LiveStrategy;202203constructor(204private readonly _editor: ICodeEditor,205@IInstantiationService private readonly _instaService: IInstantiationService,206@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,207@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,208@ILogService private readonly _logService: ILogService,209@IConfigurationService private readonly _configurationService: IConfigurationService,210@IDialogService private readonly _dialogService: IDialogService,211@IContextKeyService contextKeyService: IContextKeyService,212@IChatService private readonly _chatService: IChatService,213@IEditorService private readonly _editorService: IEditorService,214@INotebookEditorService notebookEditorService: INotebookEditorService,215@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,216@IFileService private readonly _fileService: IFileService,217@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService218) {219this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);220this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService);221this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService);222this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService);223224this._ctxResponse = ChatContextKeys.isResponse.bindTo(contextKeyService);225ChatContextKeys.responseHasError.bindTo(contextKeyService);226227this._ui = new Lazy(() => {228229const location: IChatWidgetLocationOptions = {230location: ChatAgentLocation.Editor,231resolveData: () => {232assertType(this._editor.hasModel());233assertType(this._session);234return {235type: ChatAgentLocation.Editor,236selection: this._editor.getSelection(),237document: this._session.textModelN.uri,238wholeRange: this._session?.wholeRange.trackedInitialRange,239};240}241};242243// inline chat in notebooks244// check if this editor is part of a notebook editor245// and iff so, use the notebook location but keep the resolveData246// talk about editor data247let notebookEditor: INotebookEditor | undefined;248for (const editor of notebookEditorService.listNotebookEditors()) {249for (const [, codeEditor] of editor.codeEditors) {250if (codeEditor === this._editor) {251notebookEditor = editor;252location.location = ChatAgentLocation.Notebook;253break;254}255}256}257258const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, { editor: this._editor, notebookEditor });259this._store.add(zone);260this._store.add(zone.widget.chatWidget.onDidClear(async () => {261const r = this.joinCurrentRun();262this.cancelSession();263await r;264this.run();265}));266267return zone;268});269270this._store.add(this._editor.onDidChangeModel(async e => {271if (this._session || !e.newModelUrl) {272return;273}274275const existingSession = this._inlineChatSessionService.getSession(this._editor, e.newModelUrl);276if (!existingSession) {277return;278}279280this._log('session RESUMING after model change', e);281await this.run({ existingSession });282}));283284this._store.add(this._inlineChatSessionService.onDidEndSession(e => {285if (e.session === this._session && e.endedByExternalCause) {286this._log('session ENDED by external cause');287this.acceptSession();288}289}));290291this._store.add(this._inlineChatSessionService.onDidMoveSession(async e => {292if (e.editor === this._editor) {293this._log('session RESUMING after move', e);294await this.run({ existingSession: e.session });295}296}));297298this._log(`NEW controller`);299}300301dispose(): void {302if (this._currentRun) {303this._messages.fire(this._session?.chatModel.hasRequests304? Message.PAUSE_SESSION305: Message.CANCEL_SESSION);306}307this._store.dispose();308this._isDisposed = true;309this._log('DISPOSED controller');310}311312private _log(message: string | Error, ...more: any[]): void {313if (message instanceof Error) {314this._logService.error(message, ...more);315} else {316this._logService.trace(`[IE] (editor:${this._editor.getId()}) ${message}`, ...more);317}318}319320get widget(): EditorBasedInlineChatWidget {321return this._ui.value.widget;322}323324getId(): string {325return INLINE_CHAT_ID;326}327328getWidgetPosition(): Position | undefined {329return this._ui.value.position;330}331332private _currentRun?: Promise<void>;333334async run(options: InlineChatRunOptions | undefined = {}): Promise<boolean> {335336let lastState: State | undefined;337const d = this._onDidEnterState.event(e => lastState = e);338339try {340this.acceptSession();341if (this._currentRun) {342await this._currentRun;343}344if (options.initialSelection) {345this._editor.setSelection(options.initialSelection);346}347this._stashedSession.clear();348this._currentRun = this._nextState(State.CREATE_SESSION, options);349await this._currentRun;350351} catch (error) {352// this should not happen but when it does make sure to tear down the UI and everything353this._log('error during run', error);354onUnexpectedError(error);355if (this._session) {356this._inlineChatSessionService.releaseSession(this._session);357}358this[State.PAUSE]();359360} finally {361this._currentRun = undefined;362d.dispose();363}364365return lastState !== State.CANCEL;366}367368// ---- state machine369370protected async _nextState(state: State, options: InlineChatRunOptions): Promise<void> {371let nextState: State | void = state;372while (nextState && !this._isDisposed) {373this._log('setState to ', nextState);374const p: State | Promise<State> | Promise<void> = this[nextState](options);375this._onDidEnterState.fire(nextState);376nextState = await p;377}378}379380private async [State.CREATE_SESSION](options: InlineChatRunOptions): Promise<State.CANCEL | State.INIT_UI> {381assertType(this._session === undefined);382assertType(this._editor.hasModel());383384let session: Session | undefined = options.existingSession;385386let initPosition: Position | undefined;387if (options.position) {388initPosition = Position.lift(options.position).delta(-1);389delete options.position;390}391392const widgetPosition = this._showWidget(session?.headless, true, initPosition);393394// this._updatePlaceholder();395let errorMessage = localize('create.fail', "Failed to start editor chat");396397if (!session) {398const createSessionCts = new CancellationTokenSource();399const msgListener = Event.once(this._messages.event)(m => {400this._log('state=_createSession) message received', m);401if (m === Message.ACCEPT_INPUT) {402// user accepted the input before having a session403options.autoSend = true;404this._ui.value.widget.updateInfo(localize('welcome.2', "Getting ready..."));405} else {406createSessionCts.cancel();407}408});409410try {411session = await this._inlineChatSessionService.createSession(412this._editor,413{ wholeRange: options.initialRange },414createSessionCts.token415);416} catch (error) {417// Inline chat errors are from the provider and have their error messages shown to the user418if (error instanceof InlineChatError || error?.name === InlineChatError.code) {419errorMessage = error.message;420}421}422423createSessionCts.dispose();424msgListener.dispose();425426if (createSessionCts.token.isCancellationRequested) {427if (session) {428this._inlineChatSessionService.releaseSession(session);429}430return State.CANCEL;431}432}433434delete options.initialRange;435delete options.existingSession;436437if (!session) {438MessageController.get(this._editor)?.showMessage(errorMessage, widgetPosition);439this._log('Failed to start editor chat');440return State.CANCEL;441}442443// create a new strategy444this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless);445446this._session = session;447return State.INIT_UI;448}449450private async [State.INIT_UI](options: InlineChatRunOptions): Promise<State.WAIT_FOR_INPUT | State.SHOW_REQUEST> {451assertType(this._session);452assertType(this._strategy);453454// hide/cancel inline completions when invoking IE455InlineCompletionsController.get(this._editor)?.reject();456457this._sessionStore.clear();458459const wholeRangeDecoration = this._editor.createDecorationsCollection();460const handleWholeRangeChange = () => {461const newDecorations = this._strategy?.getWholeRangeDecoration() ?? [];462wholeRangeDecoration.set(newDecorations);463464this._ctxEditing.set(!this._session?.wholeRange.trackedInitialRange.isEmpty());465};466this._sessionStore.add(toDisposable(() => {467wholeRangeDecoration.clear();468this._ctxEditing.reset();469}));470this._sessionStore.add(this._session.wholeRange.onDidChange(handleWholeRangeChange));471handleWholeRangeChange();472473this._ui.value.widget.setChatModel(this._session.chatModel);474this._updatePlaceholder();475476const isModelEmpty = !this._session.chatModel.hasRequests;477this._ui.value.widget.updateToolbar(true);478this._ui.value.widget.toggleStatus(!isModelEmpty);479this._showWidget(this._session.headless, isModelEmpty);480481this._sessionStore.add(this._editor.onDidChangeModel((e) => {482const msg = this._session?.chatModel.hasRequests483? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange484: Message.CANCEL_SESSION;485this._log('model changed, pause or cancel session', msg, e);486this._messages.fire(msg);487}));488489490this._sessionStore.add(this._editor.onDidChangeModelContent(e => {491492493if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) {494return;495}496497const wholeRange = this._session!.wholeRange;498let shouldFinishSession = false;499if (this._configurationService.getValue<boolean>(InlineChatConfigKeys.FinishOnType)) {500for (const { range } of e.changes) {501shouldFinishSession = !Range.areIntersectingOrTouching(range, wholeRange.value);502}503}504505this._session!.recordExternalEditOccurred(shouldFinishSession);506507if (shouldFinishSession) {508this._log('text changed outside of whole range, FINISH session');509this.acceptSession();510}511}));512513this._sessionStore.add(this._session.chatModel.onDidChange(async e => {514if (e.kind === 'removeRequest') {515// TODO@jrieken there is still some work left for when a request "in the middle"516// is removed. We will undo all changes till that point but not remove those517// later request518await this._session!.undoChangesUntil(e.requestId);519}520}));521522// apply edits from completed requests that haven't been applied yet523const editState = this._createChatTextEditGroupState();524let didEdit = false;525for (const request of this._session.chatModel.getRequests()) {526if (!request.response || request.response.result?.errorDetails) {527// done when seeing the first request that is still pending (no response).528break;529}530for (const part of request.response.response.value) {531if (part.kind !== 'textEditGroup' || !isEqual(part.uri, this._session.textModelN.uri)) {532continue;533}534if (part.state?.applied) {535continue;536}537for (const edit of part.edits) {538this._makeChanges(edit, undefined, !didEdit);539didEdit = true;540}541part.state ??= editState;542}543}544if (didEdit) {545const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced');546this._session.wholeRange.fixup(diff?.changes ?? []);547await this._session.hunkData.recompute(editState, diff);548549this._updateCtxResponseType();550}551options.position = await this._strategy.renderChanges();552553if (this._session.chatModel.requestInProgress) {554return State.SHOW_REQUEST;555} else {556return State.WAIT_FOR_INPUT;557}558}559560private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise<State.ACCEPT | State.CANCEL | State.PAUSE | State.WAIT_FOR_INPUT | State.SHOW_REQUEST> {561assertType(this._session);562assertType(this._strategy);563564this._updatePlaceholder();565566if (options.message) {567this._updateInput(options.message);568aria.alert(options.message);569delete options.message;570this._showWidget(this._session.headless, false);571}572573let message = Message.NONE;574let request: IChatRequestModel | undefined;575576const barrier = new Barrier();577const store = new DisposableStore();578store.add(this._session.chatModel.onDidChange(e => {579if (e.kind === 'addRequest') {580request = e.request;581message = Message.ACCEPT_INPUT;582barrier.open();583}584}));585store.add(this._strategy.onDidAccept(() => this.acceptSession()));586store.add(this._strategy.onDidDiscard(() => this.cancelSession()));587store.add(Event.once(this._messages.event)(m => {588this._log('state=_waitForInput) message received', m);589message = m;590barrier.open();591}));592593if (options.attachments) {594await Promise.all(options.attachments.map(async attachment => {595await this._ui.value.widget.chatWidget.attachmentModel.addFile(attachment);596}));597delete options.attachments;598}599if (options.autoSend) {600delete options.autoSend;601this._showWidget(this._session.headless, false);602this._ui.value.widget.chatWidget.acceptInput();603}604605await barrier.wait();606store.dispose();607608609if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) {610return State.CANCEL;611}612613if (message & Message.PAUSE_SESSION) {614return State.PAUSE;615}616617if (message & Message.ACCEPT_SESSION) {618this._ui.value.widget.selectAll();619return State.ACCEPT;620}621622if (!request?.message.text) {623return State.WAIT_FOR_INPUT;624}625626627return State.SHOW_REQUEST;628}629630631private async [State.SHOW_REQUEST](options: InlineChatRunOptions): Promise<State.WAIT_FOR_INPUT | State.CANCEL | State.PAUSE | State.ACCEPT> {632assertType(this._session);633assertType(this._strategy);634assertType(this._session.chatModel.requestInProgress);635636this._ctxRequestInProgress.set(true);637638const { chatModel } = this._session;639const request = chatModel.lastRequest;640641assertType(request);642assertType(request.response);643644this._showWidget(this._session.headless, false);645this._ui.value.widget.selectAll();646this._ui.value.widget.updateInfo('');647this._ui.value.widget.toggleStatus(true);648649const { response } = request;650const responsePromise = new DeferredPromise<void>();651652const store = new DisposableStore();653654const progressiveEditsCts = store.add(new CancellationTokenSource());655const progressiveEditsAvgDuration = new MovingAverage();656const progressiveEditsClock = StopWatch.create();657const progressiveEditsQueue = new Queue();658659// disable typing and squiggles while streaming a reply660const origDeco = this._editor.getOption(EditorOption.renderValidationDecorations);661this._editor.updateOptions({662renderValidationDecorations: 'off'663});664store.add(toDisposable(() => {665this._editor.updateOptions({666renderValidationDecorations: origDeco667});668}));669670671let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT;672store.add(Event.once(this._messages.event)(message => {673this._log('state=_makeRequest) message received', message);674this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);675if (message & Message.CANCEL_SESSION) {676next = State.CANCEL;677} else if (message & Message.PAUSE_SESSION) {678next = State.PAUSE;679} else if (message & Message.ACCEPT_SESSION) {680next = State.ACCEPT;681}682}));683684store.add(chatModel.onDidChange(async e => {685if (e.kind === 'removeRequest' && e.requestId === request.id) {686progressiveEditsCts.cancel();687responsePromise.complete();688if (e.reason === ChatRequestRemovalReason.Resend) {689next = State.SHOW_REQUEST;690} else {691next = State.CANCEL;692}693return;694}695if (e.kind === 'move') {696assertType(this._session);697const log: typeof this._log = (msg: string, ...args: any[]) => this._log('state=_showRequest) moving inline chat', msg, ...args);698699log('move was requested', e.target, e.range);700701// if there's already a tab open for targetUri, show it and move inline chat to that tab702// otherwise, open the tab to the side703const initialSelection = Selection.fromRange(Range.lift(e.range), SelectionDirection.LTR);704const editorPane = await this._editorService.openEditor({ resource: e.target, options: { selection: initialSelection } }, SIDE_GROUP);705706if (!editorPane) {707log('opening editor failed');708return;709}710711const newEditor = editorPane.getControl();712if (!isCodeEditor(newEditor) || !newEditor.hasModel()) {713log('new editor is either missing or not a code editor or does not have a model');714return;715}716717if (this._inlineChatSessionService.getSession(newEditor, e.target)) {718log('new editor ALREADY has a session');719return;720}721722const newSession = await this._inlineChatSessionService.createSession(723newEditor,724{725session: this._session,726},727CancellationToken.None); // TODO@ulugbekna: add proper cancellation?728729730InlineChatController1.get(newEditor)?.run({ existingSession: newSession });731732next = State.CANCEL;733responsePromise.complete();734735return;736}737}));738739// cancel the request when the user types740store.add(this._ui.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => {741this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);742}));743744let lastLength = 0;745let isFirstChange = true;746747const editState = this._createChatTextEditGroupState();748let localEditGroup: IChatTextEditGroup | undefined;749750// apply edits751const handleResponse = () => {752753this._updateCtxResponseType();754755if (!localEditGroup) {756localEditGroup = <IChatTextEditGroup | undefined>response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri));757}758759if (localEditGroup) {760761localEditGroup.state ??= editState;762763const edits = localEditGroup.edits;764const newEdits = edits.slice(lastLength);765if (newEdits.length > 0) {766767this._log(`${this._session?.textModelN.uri.toString()} received ${newEdits.length} edits`);768769// NEW changes770lastLength = edits.length;771progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed());772progressiveEditsClock.reset();773774progressiveEditsQueue.queue(async () => {775776const startThen = this._session!.wholeRange.value.getStartPosition();777778// making changes goes into a queue because otherwise the async-progress time will779// influence the time it takes to receive the changes and progressive typing will780// become infinitely fast781for (const edits of newEdits) {782await this._makeChanges(edits, {783duration: progressiveEditsAvgDuration.value,784token: progressiveEditsCts.token785}, isFirstChange);786787isFirstChange = false;788}789790// reshow the widget if the start position changed or shows at the wrong position791const startNow = this._session!.wholeRange.value.getStartPosition();792if (!startNow.equals(startThen) || !this._ui.value.position?.equals(startNow)) {793this._showWidget(this._session!.headless, false, startNow.delta(-1));794}795});796}797}798799if (response.isCanceled) {800progressiveEditsCts.cancel();801responsePromise.complete();802803} else if (response.isComplete) {804responsePromise.complete();805}806};807store.add(response.onDidChange(handleResponse));808handleResponse();809810// (1) we must wait for the request to finish811// (2) we must wait for all edits that came in via progress to complete812await responsePromise.p;813await progressiveEditsQueue.whenIdle();814815if (response.result?.errorDetails && !response.result.errorDetails.responseIsFiltered) {816await this._session.undoChangesUntil(response.requestId);817}818819store.dispose();820821const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced');822this._session.wholeRange.fixup(diff?.changes ?? []);823await this._session.hunkData.recompute(editState, diff);824825this._ctxRequestInProgress.set(false);826827828let newPosition: Position | undefined;829830if (response.result?.errorDetails) {831// error -> no message, errors are shown with the request832833} else if (response.response.value.length === 0) {834// empty -> show message835const status = localize('empty', "No results, please refine your input and try again");836this._ui.value.widget.updateStatus(status, { classes: ['warn'] });837838} else {839// real response -> no message840this._ui.value.widget.updateStatus('');841}842843const position = await this._strategy.renderChanges();844if (position) {845// if the selection doesn't start far off we keep the widget at its current position846// because it makes reading this nicer847const selection = this._editor.getSelection();848if (selection?.containsPosition(position)) {849if (position.lineNumber - selection.startLineNumber > 8) {850newPosition = position;851}852} else {853newPosition = position;854}855}856this._showWidget(this._session.headless, false, newPosition);857858return next;859}860861private async[State.PAUSE]() {862863this._resetWidget();864865this._strategy?.dispose?.();866this._session = undefined;867}868869private async[State.ACCEPT]() {870assertType(this._session);871assertType(this._strategy);872this._sessionStore.clear();873874try {875await this._strategy.apply();876} catch (err) {877this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err)));878this._log('FAILED to apply changes');879this._log(err);880}881882this._resetWidget();883this._inlineChatSessionService.releaseSession(this._session);884885886this._strategy?.dispose();887this._strategy = undefined;888this._session = undefined;889}890891private async[State.CANCEL]() {892893this._resetWidget();894895if (this._session) {896// assertType(this._session);897assertType(this._strategy);898this._sessionStore.clear();899900// only stash sessions that were not unstashed, not "empty", and not interacted with901const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending;902let undoCancelEdits: IValidEditOperation[] = [];903try {904undoCancelEdits = this._strategy.cancel();905} catch (err) {906this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err)));907this._log('FAILED to discard changes');908this._log(err);909}910911this._stashedSession.clear();912if (shouldStash) {913this._stashedSession.value = this._inlineChatSessionService.stashSession(this._session, this._editor, undoCancelEdits);914} else {915this._inlineChatSessionService.releaseSession(this._session);916}917}918919920this._strategy?.dispose();921this._strategy = undefined;922this._session = undefined;923}924925// ----926927private _showWidget(headless: boolean = false, initialRender: boolean = false, position?: Position) {928assertType(this._editor.hasModel());929this._ctxVisible.set(true);930931let widgetPosition: Position;932if (position) {933// explicit position wins934widgetPosition = position;935} else if (this._ui.rawValue?.position) {936// already showing - special case of line 1937if (this._ui.rawValue?.position.lineNumber === 1) {938widgetPosition = this._ui.rawValue?.position.delta(-1);939} else {940widgetPosition = this._ui.rawValue?.position;941}942} else {943// default to ABOVE the selection944widgetPosition = this._editor.getSelection().getStartPosition().delta(-1);945}946947if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) {948widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1);949}950951if (initialRender && (this._editor.getOption(EditorOption.stickyScroll)).enabled) {952this._editor.revealLine(widgetPosition.lineNumber); // do NOT substract `this._editor.getOption(EditorOption.stickyScroll).maxLineCount` because the editor already does that953}954955if (!headless) {956if (this._ui.rawValue?.position) {957this._ui.value.updatePositionAndHeight(widgetPosition);958} else {959this._ui.value.show(widgetPosition);960}961}962963return widgetPosition;964}965966private _resetWidget() {967968this._sessionStore.clear();969this._ctxVisible.reset();970971this._ui.rawValue?.hide();972973// Return focus to the editor only if the current focus is within the editor widget974if (this._editor.hasWidgetFocus()) {975this._editor.focus();976}977}978979private _updateCtxResponseType(): void {980981if (!this._session) {982this._ctxResponseType.set(InlineChatResponseType.None);983return;984}985986const hasLocalEdit = (response: IResponse): boolean => {987return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri));988};989990let responseType = InlineChatResponseType.None;991for (const request of this._session.chatModel.getRequests()) {992if (!request.response) {993continue;994}995responseType = InlineChatResponseType.Messages;996if (hasLocalEdit(request.response.response)) {997responseType = InlineChatResponseType.MessagesAndEdits;998break; // no need to check further999}1000}1001this._ctxResponseType.set(responseType);1002this._ctxResponse.set(responseType !== InlineChatResponseType.None);1003}10041005private _createChatTextEditGroupState(): IChatTextEditGroupState {1006assertType(this._session);10071008const sha1 = new DefaultModelSHA1Computer();1009const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0)1010? sha1.computeSHA1(this._session.textModel0)1011: generateUuid();10121013return {1014sha1: textModel0Sha1,1015applied: 01016};1017}10181019private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) {1020assertType(this._session);1021assertType(this._strategy);10221023const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits);1024this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits);10251026if (moreMinimalEdits?.length === 0) {1027// nothing left to do1028return;1029}10301031const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits;1032const editOperations = actualEdits.map(TextEdit.asEditOperation);10331034const editsObserver: IEditObserver = {1035start: () => this._session!.hunkData.ignoreTextModelNChanges = true,1036stop: () => this._session!.hunkData.ignoreTextModelNChanges = false,1037};10381039const metadata = this._getMetadata();1040if (opts) {1041await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore, metadata);1042} else {1043await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore, metadata);1044}1045}10461047private _getMetadata(): IInlineChatMetadata {1048const lastRequest = this._session?.chatModel.lastRequest;1049return {1050extensionId: VersionedExtensionId.tryCreate(this._session?.agent.extensionId.value, this._session?.agent.extensionVersion),1051modelId: lastRequest?.modelId,1052requestId: lastRequest?.id,1053};1054}10551056private _updatePlaceholder(): void {1057this._ui.value.widget.placeholder = this._session?.agent.description ?? localize('askOrEditInContext', 'Ask or edit in context');1058}10591060private _updateInput(text: string, selectAll = true): void {10611062this._ui.value.widget.chatWidget.setInput(text);1063if (selectAll) {1064const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1);1065this._ui.value.widget.chatWidget.inputEditor.setSelection(newSelection);1066}1067}10681069// ---- controller API10701071arrowOut(up: boolean): void {1072if (this._ui.value.position && this._editor.hasModel()) {1073const { column } = this._editor.getPosition();1074const { lineNumber } = this._ui.value.position;1075const newLine = up ? lineNumber : lineNumber + 1;1076this._editor.setPosition({ lineNumber: newLine, column });1077this._editor.focus();1078}1079}10801081focus(): void {1082this._ui.value.widget.focus();1083}10841085async viewInChat() {1086if (!this._strategy || !this._session) {1087return;1088}10891090let someApplied = false;1091let lastEdit: IChatTextEditGroup | undefined;10921093const uri = this._editor.getModel()?.uri;1094const requests = this._session.chatModel.getRequests();1095for (const request of requests) {1096if (!request.response) {1097continue;1098}1099for (const part of request.response.response.value) {1100if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) {1101// fully or partially applied edits1102someApplied = someApplied || Boolean(part.state?.applied);1103lastEdit = part;1104part.edits = [];1105part.state = undefined;1106}1107}1108}11091110const doEdits = this._strategy.cancel();11111112if (someApplied) {1113assertType(lastEdit);1114lastEdit.edits = [doEdits];1115}11161117await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel);11181119this.cancelSession();1120}11211122acceptSession(): void {1123const response = this._session?.chatModel.getRequests().at(-1)?.response;1124if (response) {1125this._chatService.notifyUserAction({1126sessionId: response.session.sessionId,1127requestId: response.requestId,1128agentId: response.agent?.id,1129command: response.slashCommand?.name,1130result: response.result,1131action: {1132kind: 'inlineChat',1133action: 'accepted'1134}1135});1136}1137this._messages.fire(Message.ACCEPT_SESSION);1138}11391140acceptHunk(hunkInfo?: HunkInformation) {1141return this._strategy?.performHunkAction(hunkInfo, HunkAction.Accept);1142}11431144discardHunk(hunkInfo?: HunkInformation) {1145return this._strategy?.performHunkAction(hunkInfo, HunkAction.Discard);1146}11471148toggleDiff(hunkInfo?: HunkInformation) {1149return this._strategy?.performHunkAction(hunkInfo, HunkAction.ToggleDiff);1150}11511152moveHunk(next: boolean) {1153this.focus();1154this._strategy?.performHunkAction(undefined, next ? HunkAction.MoveNext : HunkAction.MovePrev);1155}11561157async cancelSession() {1158const response = this._session?.chatModel.lastRequest?.response;1159if (response) {1160this._chatService.notifyUserAction({1161sessionId: response.session.sessionId,1162requestId: response.requestId,1163agentId: response.agent?.id,1164command: response.slashCommand?.name,1165result: response.result,1166action: {1167kind: 'inlineChat',1168action: 'discarded'1169}1170});1171}11721173this._messages.fire(Message.CANCEL_SESSION);1174}11751176reportIssue() {1177const response = this._session?.chatModel.lastRequest?.response;1178if (response) {1179this._chatService.notifyUserAction({1180sessionId: response.session.sessionId,1181requestId: response.requestId,1182agentId: response.agent?.id,1183command: response.slashCommand?.name,1184result: response.result,1185action: { kind: 'bug' }1186});1187}1188}11891190unstashLastSession(): Session | undefined {1191const result = this._stashedSession.value?.unstash();1192return result;1193}11941195joinCurrentRun(): Promise<void> | undefined {1196return this._currentRun;1197}11981199get isActive() {1200return Boolean(this._currentRun);1201}12021203async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {1204if (attachment.scheme === Schemas.file) {1205if (await this._fileService.canHandleResource(attachment)) {1206return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);1207}1208} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {1209const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);1210if (extractedImages) {1211return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);1212}1213}12141215return undefined;1216}1217}12181219export class InlineChatController2 implements IEditorContribution {12201221static readonly ID = 'editor.contrib.inlineChatController2';12221223static get(editor: ICodeEditor): InlineChatController2 | undefined {1224return editor.getContribution<InlineChatController2>(InlineChatController2.ID) ?? undefined;1225}12261227private readonly _store = new DisposableStore();1228private readonly _showWidgetOverrideObs = observableValue(this, false);1229private readonly _isActiveController = observableValue(this, false);1230private readonly _zone: Lazy<InlineChatZoneWidget>;12311232private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;12331234get widget(): EditorBasedInlineChatWidget {1235return this._zone.value.widget;1236}12371238get isActive() {1239return Boolean(this._currentSession.get());1240}12411242constructor(1243private readonly _editor: ICodeEditor,1244@IInstantiationService private readonly _instaService: IInstantiationService,1245@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,1246@IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService,1247@ICodeEditorService codeEditorService: ICodeEditorService,1248@IContextKeyService contextKeyService: IContextKeyService,1249@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,1250@IFileService private readonly _fileService: IFileService,1251@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,1252@IEditorService private readonly _editorService: IEditorService,1253@IInlineChatSessionService inlineChatService: IInlineChatSessionService,1254@IConfigurationService configurationService: IConfigurationService,1255) {12561257const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);12581259this._zone = new Lazy<InlineChatZoneWidget>(() => {126012611262const location: IChatWidgetLocationOptions = {1263location: ChatAgentLocation.Editor,1264resolveData: () => {1265assertType(this._editor.hasModel());12661267return {1268type: ChatAgentLocation.Editor,1269selection: this._editor.getSelection(),1270document: this._editor.getModel().uri,1271wholeRange: this._editor.getSelection(),1272};1273}1274};12751276// inline chat in notebooks1277// check if this editor is part of a notebook editor1278// if so, update the location and use the notebook specific widget1279let notebookEditor: INotebookEditor | undefined;1280for (const editor of this._notebookEditorService.listNotebookEditors()) {1281for (const [, codeEditor] of editor.codeEditors) {1282if (codeEditor === this._editor) {1283location.location = ChatAgentLocation.Notebook;1284notebookEditor = editor;1285// set location2 so that the notebook agent intent is used1286if (configurationService.getValue(InlineChatConfigKeys.notebookAgent)) {1287location.resolveData = () => {1288assertType(this._editor.hasModel());12891290return {1291type: ChatAgentLocation.Notebook,1292sessionInputUri: this._editor.getModel().uri,1293};1294};1295}12961297break;1298}1299}1300}13011302const result = this._instaService.createInstance(InlineChatZoneWidget,1303location,1304{1305enableWorkingSet: 'implicit',1306rendererOptions: {1307renderTextEditsAsSummary: _uri => true1308}1309},1310{ editor: this._editor, notebookEditor },1311);13121313result.domNode.classList.add('inline-chat-2');13141315return result;1316});131713181319const editorObs = observableCodeEditor(_editor);13201321const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessions.onDidChangeSessions);13221323this._currentSession = derived(r => {1324sessionsSignal.read(r);1325const model = editorObs.model.read(r);1326const value = model && _inlineChatSessions.getSession2(model.uri);1327return value ?? undefined;1328});132913301331this._store.add(autorun(r => {1332const session = this._currentSession.read(r);1333if (!session) {1334this._isActiveController.set(false, undefined);1335return;1336}1337let foundOne = false;1338for (const editor of codeEditorService.listCodeEditors()) {1339if (Boolean(InlineChatController2.get(editor)?._isActiveController.get())) {1340foundOne = true;1341break;1342}1343}1344if (!foundOne && editorObs.isFocused.read(r)) {1345this._isActiveController.set(true, undefined);1346}1347}));13481349const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);13501351this._store.add(autorunWithStore((r, store) => {13521353const model = editorObs.model.read(r);1354const session = this._currentSession.read(r);1355const isActive = this._isActiveController.read(r);13561357if (!session || !isActive || !model) {1358visibleSessionObs.set(undefined, undefined);1359return;1360}13611362const { chatModel } = session;1363const showShowUntil = this._showWidgetOverrideObs.read(r);1364const hasNoRequests = chatModel.getRequests().length === 0;1365const hideOnRequest = inlineChatService.hideOnRequest.read(r);13661367const responseListener = store.add(new MutableDisposable());13681369if (hideOnRequest) {1370// hide the request once the request has been added, reveal it again when no edit was made1371// or when an error happened1372store.add(chatModel.onDidChange(e => {1373if (e.kind === 'addRequest') {1374transaction(tx => {1375this._showWidgetOverrideObs.set(false, tx);1376visibleSessionObs.set(undefined, tx);1377});1378const { response } = e.request;1379if (!response) {1380return;1381}1382responseListener.value = response.onDidChange(async e => {13831384if (!response.isComplete) {1385return;1386}13871388const shouldShow = response.isCanceled // cancelled1389|| response.result?.errorDetails // errors1390|| !response.response.value.find(part => part.kind === 'textEditGroup'1391&& part.edits.length > 01392&& isEqual(part.uri, model.uri)); // NO edits for file13931394if (shouldShow) {1395visibleSessionObs.set(session, undefined);1396}1397});1398}1399}));1400}14011402if (showShowUntil || hasNoRequests || !hideOnRequest) {1403visibleSessionObs.set(session, undefined);1404} else {1405visibleSessionObs.set(undefined, undefined);1406}1407}));14081409this._store.add(autorun(r => {14101411const session = visibleSessionObs.read(r);14121413if (!session) {1414this._zone.rawValue?.hide();1415_editor.focus();1416ctxInlineChatVisible.reset();1417} else {1418ctxInlineChatVisible.set(true);1419this._zone.value.widget.setChatModel(session.chatModel);1420if (!this._zone.value.position) {1421this._zone.value.show(session.initialPosition);1422}1423this._zone.value.reveal(this._zone.value.position!);1424this._zone.value.widget.focus();1425this._zone.value.widget.updateToolbar(true);1426const entry = session.editingSession.getEntry(session.uri);14271428entry?.autoAcceptController.get()?.cancel();14291430const requestCount = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().length).read(r);1431this._zone.value.widget.updateToolbar(requestCount > 0);1432}1433}));14341435this._store.add(autorun(r => {14361437const session = visibleSessionObs.read(r);1438const entry = session?.editingSession.readEntry(session.uri, r);1439const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));1440if (pane && entry) {1441entry?.getEditorIntegration(pane);1442}1443}));1444}14451446dispose(): void {1447this._store.dispose();1448}14491450toggleWidgetUntilNextRequest() {1451const value = this._showWidgetOverrideObs.get();1452this._showWidgetOverrideObs.set(!value, undefined);1453}14541455getWidgetPosition(): Position | undefined {1456return this._zone.rawValue?.position;1457}14581459focus() {1460this._zone.rawValue?.widget.focus();1461}14621463markActiveController() {1464this._isActiveController.set(true, undefined);1465}14661467async run(arg?: InlineChatRunOptions): Promise<boolean> {1468assertType(this._editor.hasModel());14691470this.markActiveController();14711472const uri = this._editor.getModel().uri;1473const session = this._inlineChatSessions.getSession2(uri)1474?? await this._inlineChatSessions.createSession2(this._editor, uri, CancellationToken.None);14751476if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {1477if (arg.initialRange) {1478this._editor.revealRange(arg.initialRange);1479}1480if (arg.initialSelection) {1481this._editor.setSelection(arg.initialSelection);1482}1483if (arg.attachments) {1484await Promise.all(arg.attachments.map(async attachment => {1485await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);1486}));1487delete arg.attachments;1488}1489if (arg.message) {1490this._zone.value.widget.chatWidget.setInput(arg.message);1491if (arg.autoSend) {1492await this._zone.value.widget.chatWidget.acceptInput();1493}1494}1495}14961497await Event.toPromise(session.editingSession.onDidDispose);14981499const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected;1500return !rejected;1501}15021503acceptSession() {1504const value = this._currentSession.get();1505value?.editingSession.accept();1506}15071508async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {1509const value = this._currentSession.get();1510if (!value) {1511return undefined;1512}1513if (attachment.scheme === Schemas.file) {1514if (await this._fileService.canHandleResource(attachment)) {1515return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);1516}1517} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {1518const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);1519if (extractedImages) {1520return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);1521}1522}1523return undefined;1524}1525}15261527export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable<TextEdit[]>, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {1528if (!editor.hasModel()) {1529return false;1530}15311532const chatService = accessor.get(IChatService);1533const uri = editor.getModel().uri;1534const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false);15351536chatModel.startEditingSession(true);15371538const editSession = await chatModel.editingSessionObs?.promise;15391540const store = new DisposableStore();1541store.add(chatModel);15421543// STREAM1544const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, {1545kind: undefined,1546modeId: 'applyCodeBlock',1547instructions: undefined,1548isBuiltin: true,1549applyCodeBlockSuggestionId,1550});1551assertType(chatRequest.response);1552chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });1553for await (const chunk of stream) {15541555if (token.isCancellationRequested) {1556chatRequest.response.cancel();1557break;1558}15591560chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false });1561}1562chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });15631564if (!token.isCancellationRequested) {1565chatModel.completeResponse(chatRequest);1566}15671568const isSettled = derived(r => {1569const entry = editSession?.readEntry(uri, r);1570if (!entry) {1571return false;1572}1573const state = entry.state.read(r);1574return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;1575});1576const whenDecided = waitForState(isSettled, Boolean);1577await raceCancellation(whenDecided, token);1578store.dispose();1579return true;1580}15811582export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise<boolean> {15831584const chatService = accessor.get(IChatService);1585const notebookService = accessor.get(INotebookService);1586const isNotebook = notebookService.hasSupportedNotebooks(uri);1587const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false);15881589chatModel.startEditingSession(true);15901591const editSession = await chatModel.editingSessionObs?.promise;15921593const store = new DisposableStore();1594store.add(chatModel);15951596// STREAM1597const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0);1598assertType(chatRequest.response);1599if (isNotebook) {1600chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false });1601} else {1602chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });1603}1604for await (const chunk of stream) {16051606if (token.isCancellationRequested) {1607chatRequest.response.cancel();1608break;1609}1610if (chunk.every(isCellEditOperation)) {1611chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false });1612} else {1613chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false });1614}1615}1616if (isNotebook) {1617chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true });1618} else {1619chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });1620}16211622if (!token.isCancellationRequested) {1623chatRequest.response.complete();1624}16251626const isSettled = derived(r => {1627const entry = editSession?.readEntry(uri, r);1628if (!entry) {1629return false;1630}1631const state = entry.state.read(r);1632return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;1633});16341635const whenDecided = waitForState(isSettled, Boolean);16361637await raceCancellation(whenDecided, token);16381639store.dispose();16401641return true;1642}16431644function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation {1645if (URI.isUri(edit)) {1646return false;1647}1648if (Array.isArray(edit)) {1649return false;1650}1651return true;1652}16531654async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) {16551656const viewsService = accessor.get(IViewsService);1657const chatService = accessor.get(IChatService);16581659const widget = await showChatView(viewsService);16601661if (widget && widget.viewModel && model) {1662for (const request of model.getRequests().slice()) {1663await chatService.adoptRequest(widget.viewModel.model.sessionId, request);1664}1665widget.focusLastMessage();1666}1667}166816691670