diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index eb2d7e2a9fd..f3d4decc2cb 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -28,11 +28,12 @@ import { ChatAgentLocation, IChatAgentHistoryEntry, IChatAgentImplementation, IC import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js'; import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js'; import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js'; -import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; import { IExtensionService } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostChatAgentsShape2, ExtHostContext, IChatParticipantMetadata, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js'; +import { ExtHostChatAgentsShape2, ExtHostContext, IChatNotebookEditDto, IChatParticipantMetadata, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadChatAgentsShape2 } from '../common/extHost.protocol.js'; +import { NotebookDto } from './mainThreadNotebookDto.js'; interface AgentData { dispose: () => void; @@ -225,7 +226,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA } async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { - const revivedProgress = revive(progress) as IChatProgress; + const revivedProgress = progress.kind === 'notebookEdit' ? ChatNotebookEdit.fromChatEdit(revive(progress)) : revive(progress) as IChatProgress; if (revivedProgress.kind === 'progressTask') { const handle = ++this._responsePartHandlePool; const responsePartId = `${requestId}_${handle}`; @@ -385,3 +386,37 @@ function computeCompletionRanges(model: ITextModel, position: Position, reg: Reg return { insert, replace }; } + +namespace ChatNotebookEdit { + export function fromChatEdit(part: IChatNotebookEditDto): IChatNotebookEdit { + return { + kind: 'notebookEdit', + uri: part.uri, + done: part.done, + edits: part.edits.map(e => { + return { + count: e.count, + editType: e.editType, + index: e.index, + cells: e.cells.map(NotebookDto.fromNotebookCellDataDto), + }; + }) + }; + } + export function toChatEdit(part: IChatNotebookEdit): IChatNotebookEditDto { + return { + kind: 'notebookEdit', + done: part.done, + uri: URI.revive(part.uri), + edits: part.edits.map(e => { + return { + count: e.count, + editType: e.editType, + index: e.index, + cells: e.cells.map(NotebookDto.toNotebookCellDataDto) + }; + }) + }; + } + +} diff --git a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts index f447d2ca755..97f665ea169 100644 --- a/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts +++ b/src/vs/workbench/api/browser/mainThreadChatCodeMapper.ts @@ -5,9 +5,12 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Disposable, DisposableMap, IDisposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; +import { TextEdit } from '../../../editor/common/languages.js'; import { ICodeMapperProvider, ICodeMapperRequest, ICodeMapperResponse, ICodeMapperService } from '../../contrib/chat/common/chatCodeMapperService.js'; +import { CellEditType, ICellEditOperation } from '../../contrib/notebook/common/notebookCommon.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { ExtHostCodeMapperShape, ExtHostContext, ICodeMapperProgressDto, ICodeMapperRequestDto, MainContext, MainThreadCodeMapperShape } from '../common/extHost.protocol.js'; +import { NotebookDto } from './mainThreadNotebookDto.js'; @extHostNamedCustomer(MainContext.MainThreadCodeMapper) export class MainThreadChatCodemapper extends Disposable implements MainThreadCodeMapperShape { @@ -56,9 +59,35 @@ export class MainThreadChatCodemapper extends Disposable implements MainThreadCo $handleProgress(requestId: string, data: ICodeMapperProgressDto): Promise { const response = this._responseMap.get(requestId); if (response) { + const edits = data.edits; const resource = URI.revive(data.uri); - response.textEdit(resource, data.edits); + if (!edits.length) { + response.textEdit(resource, []); + } else if (areTextEdits(edits)) { + response.textEdit(resource, edits); + } else { + const cellEdits: ICellEditOperation[] = []; + edits.forEach(dto => { + if (dto.editType === CellEditType.Replace) { + cellEdits.push({ + editType: dto.editType, + index: dto.index, + count: dto.count, + cells: dto.cells.map(NotebookDto.fromNotebookCellDataDto) + }); + } + }); + response.notebookEdit(resource, cellEdits); + } } return Promise.resolve(); } } + +function areTextEdits(edits: ICodeMapperProgressDto['edits']): edits is TextEdit[] { + if (edits.some(e => 'range' in e && 'text' in e)) { + return true; + } else { + return false; + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c9315349487..9fa1bdc0a86 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1761,6 +1761,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseCodeblockUriPart: extHostTypes.ChatResponseCodeblockUriPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, + ChatResponseNotebookEditPart: extHostTypes.ChatResponseNotebookEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart, ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 40c2f58eeba..d15018a6636 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -54,7 +54,7 @@ import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentRes import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatRelatedFile, IChatRelatedFileProviderMetadata as IChatRelatedFilesProviderMetadata, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatProgressHistoryResponseContent } from '../../contrib/chat/common/chatModel.js'; -import { IChatContentInlineReference, IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; +import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; import { IPreparedToolInvocation, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; @@ -1313,7 +1313,12 @@ export interface ICodeMapperTextEdit { edits: languages.TextEdit[]; } -export type ICodeMapperProgressDto = Dto; +export type ICodeMapperProgressDto = Dto | ICodeMapperNotebookEditDto; + +export interface ICodeMapperNotebookEditDto { + uri: URI; + edits: ICellEditOperationDto[]; +} export interface MainThreadCodeMapperShape extends IDisposable { $registerCodeMapperProvider(handle: number, displayName: string): void; @@ -1423,8 +1428,9 @@ export type IDocumentContextDto = { }; export type IChatProgressDto = - | Dto> - | IChatTaskDto; + | Dto> + | IChatTaskDto + | IChatNotebookEditDto; export interface ExtHostUrlsShape { $handleExternalUri(handle: number, uri: UriComponents): Promise; @@ -2140,6 +2146,19 @@ export interface IWorkspaceEditEntryMetadataDto { iconPath?: { id: string } | UriComponents | { light: UriComponents; dark: UriComponents }; } +export interface IChatNotebookEditDto { + uri: URI; + edits: ICellEditReplaceOperationDto[]; + kind: 'notebookEdit'; + done?: boolean; +} + +export type ICellEditReplaceOperationDto = { + editType: notebookCommon.CellEditType.Replace; + index: number; + count: number; + cells: NotebookCellDataDto[]; +}; export type ICellEditOperationDto = notebookCommon.ICellMetadataEdit diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index fadac236faf..3313b2ce231 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -224,6 +224,15 @@ class ChatAgentResponseStream { _report(dto); return this; }, + notebookEdit(target, edits) { + throwIfDone(this.notebookEdit); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + + const part = new extHostTypes.ChatResponseNotebookEditPart(target, Array.isArray(edits) ? edits : []); + const dto = typeConvert.ChatResponseNotebookEditPart.from(part); + _report(dto); + return this; + }, confirmation(title, message, data, buttons) { throwIfDone(this.confirmation); checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); @@ -238,6 +247,7 @@ class ChatAgentResponseStream { if ( part instanceof extHostTypes.ChatResponseTextEditPart || + part instanceof extHostTypes.ChatResponseNotebookEditPart || part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || part instanceof extHostTypes.ChatResponseWarningPart || part instanceof extHostTypes.ChatResponseConfirmationPart || diff --git a/src/vs/workbench/api/common/extHostCodeMapper.ts b/src/vs/workbench/api/common/extHostCodeMapper.ts index ffa51e9a759..8609364f82e 100644 --- a/src/vs/workbench/api/common/extHostCodeMapper.ts +++ b/src/vs/workbench/api/common/extHostCodeMapper.ts @@ -8,8 +8,9 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import * as extHostProtocol from './extHost.protocol.js'; -import { TextEdit } from './extHostTypeConverters.js'; +import { NotebookEdit, TextEdit } from './extHostTypeConverters.js'; import { URI } from '../../../base/common/uri.js'; +import { isDefined } from '../../../base/common/types.js'; export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape { @@ -38,6 +39,13 @@ export class ExtHostCodeMapper implements extHostProtocol.ExtHostCodeMapperShape uri: target, edits: edits.map(TextEdit.from) }); + }, + notebookEdit: (target: vscode.Uri, edits: vscode.NotebookEdit | vscode.NotebookEdit[]) => { + edits = (Array.isArray(edits) ? edits : [edits]); + this._proxy.$handleProgress(internalRequest.requestId, { + uri: target, + edits: edits.map(NotebookEdit.toEditReplaceOperation).filter(isDefined) + }); } }; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index cde6b2dee05..c63e81dd754 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -2600,6 +2600,46 @@ export namespace ChatResponseTextEditPart { } +export namespace NotebookEdit { + export function toEditReplaceOperation(edit: vscode.NotebookEdit): Dto { + // We are only interested in cell replaces (insertions, deletions, replacements) + if (!edit.newCellMetadata && !edit.newNotebookMetadata) { + return { + editType: notebooks.CellEditType.Replace, + index: edit.range.start, + count: edit.range.end - edit.range.start, + cells: edit.newCells.map(NotebookCellData.from) + }; + } + return undefined; + } + + export function fromEditReplaceOperation(edit: Dto): vscode.NotebookEdit { + return new types.NotebookEdit(new types.NotebookRange(edit.index, edit.index + edit.count), edit.cells.map(NotebookCellData.to)); + } +} + + +export namespace ChatResponseNotebookEditPart { + export function from(part: vscode.ChatResponseNotebookEditPart): extHostProtocol.IChatNotebookEditDto { + return { + kind: 'notebookEdit', + uri: URI.revive(part.uri), + // We are only interested in cell replaces (insertions, deletions, replacements) + edits: part.edits.map(e => NotebookEdit.toEditReplaceOperation(e)).filter(isDefined), + done: part.isDone + }; + } + + export function to(part: extHostProtocol.IChatNotebookEditDto): vscode.ChatResponseNotebookEditPart { + if (part.done) { + return new types.ChatResponseNotebookEditPart(URI.revive(part.uri), true); + } else { + return new types.ChatResponseNotebookEditPart(URI.revive(part.uri), part.edits.map(NotebookEdit.fromEditReplaceOperation)); + } + } +} + export namespace ChatResponseReferencePart { export function from(part: types.ChatResponseReferencePart): Dto { const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath @@ -2675,6 +2715,8 @@ export namespace ChatResponsePart { return ChatResponseCommandButtonPart.from(part, commandsConverter, commandDisposables); } else if (part instanceof types.ChatResponseTextEditPart) { return ChatResponseTextEditPart.from(part); + } else if (part instanceof types.ChatResponseNotebookEditPart) { + return ChatResponseNotebookEditPart.from(part); } else if (part instanceof types.ChatResponseMarkdownWithVulnerabilitiesPart) { return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); } else if (part instanceof types.ChatResponseCodeblockUriPart) { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 826548dfdef..967ba0632e3 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4639,6 +4639,22 @@ export class ChatResponseTextEditPart implements vscode.ChatResponseTextEditPart } } +export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebookEditPart { + uri: vscode.Uri; + edits: vscode.NotebookEdit[]; + isDone?: boolean; + constructor(uri: vscode.Uri, editsOrDone: vscode.NotebookEdit | vscode.NotebookEdit[] | true) { + this.uri = uri; + if (editsOrDone === true) { + this.isDone = true; + this.edits = []; + } else { + this.edits = Array.isArray(editsOrDone) ? editsOrDone : [editsOrDone]; + + } + } +} + export class ChatRequestTurn implements vscode.ChatRequestTurn { constructor( readonly prompt: string, diff --git a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts index 8831b488004..f6d1050cbd5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/codeBlockOperations.ts @@ -273,7 +273,10 @@ export class ApplyCodeBlockOperation { const response: ICodeMapperResponse = { textEdit: (target: URI, edit: TextEdit[]) => { executor.emitOne(edit); - } + }, + notebookEdit(_resource, _edit) { + // + }, }; const result = await this.codeMapperService.mapCode(request, response, token); if (result?.errorMessage) { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatNtoebookEditContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatNtoebookEditContentPart.ts new file mode 100644 index 00000000000..0482fb77954 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatNtoebookEditContentPart.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { assertType } from '../../../../../base/common/types.js'; +import { localize } from '../../../../../nls.js'; +import { IChatListItemRendererOptions } from '../chat.js'; +import { IDisposableReference } from './chatCollections.js'; +import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js'; +import { CodeCompareBlockPart } from '../codeBlockPart.js'; +import { IChatNotebookEditGroup, IChatProgressRenderableResponseContent } from '../../common/chatModel.js'; +import { isResponseVM } from '../../common/chatViewModel.js'; + +const $ = dom.$; + +export class ChatNotebookEditContentPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + private readonly comparePart: IDisposableReference | undefined; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + public readonly onDidChangeHeight = this._onDidChangeHeight.event; + + constructor( + chatTextEdit: IChatNotebookEditGroup, + context: IChatContentPartRenderContext, + rendererOptions: IChatListItemRendererOptions, + ) { + super(); + const element = context.element; + + assertType(isResponseVM(element)); + + // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen + if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { + if (element.response.value.every(item => item.kind === 'notebookEditGroup')) { + this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete + ? '' + : element.isCanceled + ? localize('edits0', "Making changes was aborted.") + : localize('editsSummary', "Made changes.")); + } else { + this.domNode = $('div'); + } + } else { + this.domNode = $('div'); + } + } + + layout(width: number): void { + this.comparePart?.object.layout(width); + } + + hasSameContent(other: IChatProgressRenderableResponseContent): boolean { + // No other change allowed for this content type + return other.kind === 'notebookEditGroup'; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 5face9c3717..bcaf91db061 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -13,7 +13,6 @@ import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { LinkedList } from '../../../../../base/common/linkedList.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { Schemas } from '../../../../../base/common/network.js'; import { derived, IObservable, observableValueOpts, runOnChange, ValueWithChangeEventFromObservable } from '../../../../../base/common/observable.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; @@ -36,8 +35,8 @@ import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMul import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, WorkingSetEntryState } from '../../common/chatEditingService.js'; -import { IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; +import { IChatNotebookEditGroup, IChatResponseModel, IChatTextEditGroup } from '../../common/chatModel.js'; +import { ICellEditReplaceOperation, IChatService } from '../../common/chatService.js'; import { ChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -224,7 +223,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const observerDisposables = new DisposableStore(); - let editsSource: AsyncIterableSource | undefined; + let editsSource: AsyncIterableSource | undefined; let editsPromise: Promise | undefined; const editsSeen = new ResourceMap<{ seen: number }>(); const editedFilesExist = new ResourceMap>(); @@ -249,14 +248,13 @@ export class ChatEditingService extends Disposable implements IChatEditingServic } for (const part of responseModel.response.value) { - if (part.kind !== 'codeblockUri' && part.kind !== 'textEditGroup') { + if (part.kind !== 'codeblockUri' && part.kind !== 'textEditGroup' && part.kind !== 'notebookEditGroup') { continue; } // ensure editor is open asap - if (!editedFilesExist.get(part.uri)) { - const uri = part.uri.scheme === Schemas.vscodeNotebookCell ? CellUri.parse(part.uri)?.notebook ?? part.uri : part.uri; - - editedFilesExist.set(part.uri, this._fileService.exists(uri).then((e) => { + const uri = CellUri.parse(part.uri)?.notebook || part.uri; + if (!editedFilesExist.get(uri)) { + editedFilesExist.set(uri, this._fileService.exists(uri).then((e) => { if (!e) { return; } @@ -278,6 +276,30 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const newEdits = allEdits.slice(entry.seen); entry.seen += newEdits.length; + if (part.kind === 'notebookEditGroup') { + const allEdits: ICellEditReplaceOperation[][] = part.edits; + // The -1 is not great, we sent a notebook edit with an empty array to indicate the start of the edit for this notebook. + // Perhaps we should not send an empty edit for the noteobook Uri, + // instead we should sent an empty notebook edit for the notebook Uri. + const newEdits = allEdits.slice(entry.seen - 1); + entry.seen += newEdits.length; + if (newEdits.length > 0 || (entry.seen - 1) === 0) { + // only allow empty edits when having just started, ignore otherwise to avoid unneccessary work + editsSource ??= new AsyncIterableSource(); + editsSource.emitOne({ uri: part.uri, edits: newEdits, kind: 'notebookEditGroup', done: part.kind === 'notebookEditGroup' && part.done }); + } + } else { + const allEdits: TextEdit[][] = part.kind === 'textEditGroup' ? part.edits : []; + const newEdits = allEdits.slice(entry.seen); + entry.seen += newEdits.length; + + if (newEdits.length > 0 || entry.seen === 0) { + // only allow empty edits when having just started, ignore otherwise to avoid unneccessary work + editsSource ??= new AsyncIterableSource(); + editsSource.emitOne({ uri: part.uri, edits: newEdits, kind: 'textEditGroup', done: part.kind === 'textEditGroup' && part.done }); + } + } + if (newEdits.length > 0 || entry.seen === 0) { // only allow empty edits when having just started, ignore otherwise to avoid unneccessary work editsSource ??= new AsyncIterableSource(); @@ -299,9 +321,14 @@ export class ChatEditingService extends Disposable implements IChatEditingServic continue; } for (let i = 0; i < item.edits.length; i++) { - const group = item.edits[i]; const isLastGroup = i === item.edits.length - 1; - builder.textEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); + if (item.kind === 'notebookEditGroup') { + const group = item.edits[i]; + builder.notebookEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); + } else { + const group = item.edits[i]; + builder.textEdits(item.uri, group, isLastGroup && (item.done ?? false), responseModel); + } } } }).finally(() => { @@ -350,7 +377,10 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const stream: IChatEditingSessionStream = { textEdits: (resource: URI, textEdits: TextEdit[], isDone: boolean, responseModel: IChatResponseModel) => { session.acceptTextEdits(resource, textEdits, isDone, responseModel); - } + }, + notebookEdits(resource: URI, edits: ICellEditReplaceOperation[], isLastEdits: boolean, responseModel: IChatResponseModel) { + session.acceptNotebookEdits(resource, edits, isLastEdits, responseModel); + }, }; session.acceptStreamingEditsStart(); try { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index edf3711eaeb..d486e6e8928 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -42,7 +42,7 @@ import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorIn import { INotebookService } from '../../../notebook/common/notebookService.js'; import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedFileEntry, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; +import { ICellEditReplaceOperation, IChatService } from '../../common/chatService.js'; import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; @@ -625,6 +625,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio // ensure that the edits are processed sequentially this._sequencer.queue(() => this._acceptTextEdits(resource, textEdits, isLastEdits, responseModel)); } + acceptNotebookEdits(_resource: URI, _edits: ICellEditReplaceOperation[], _isLastEdits: boolean, _responseModel: IChatResponseModel): void { + // + } resolve(): void { if (this._state.get() === ChatEditingSessionState.Disposed) { diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 1a31841462c..57c617a5d10 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -44,7 +44,7 @@ import { IWorkbenchIssueService } from '../../issue/common/issue.js'; import { annotateSpecialMarkdownContent } from '../common/annotations.js'; import { ChatAgentLocation, IChatAgentMetadata } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; +import { IChatNotebookEditGroup, IChatRequestVariableEntry, IChatTextEditGroup } from '../common/chatModel.js'; import { chatSubcommandLeader } from '../common/chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatConfirmation, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatTask, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData } from '../common/chatService.js'; import { IChatCodeCitations, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../common/chatViewModel.js'; @@ -60,6 +60,7 @@ import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandCont import { ChatConfirmationContentPart } from './chatContentParts/chatConfirmationContentPart.js'; import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts/chatContentParts.js'; import { ChatMarkdownContentPart, EditorPool } from './chatContentParts/chatMarkdownContentPart.js'; +import { ChatNotebookEditContentPart } from './chatContentParts/chatNtoebookEditContentPart.js'; import { ChatProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart.js'; import { ChatCollapsibleListContentPart, CollapsibleListPool } from './chatContentParts/chatReferencesContentPart.js'; @@ -834,6 +835,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + textEditPart.layout(this._currentLayoutWidth); + this.updateItemHeight(templateData); + })); + + return textEditPart; + } + private renderMarkdown(markdown: IChatMarkdownContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { const element = context.element; const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData); diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index dc14fa50f44..fe13bc97df6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -298,6 +298,14 @@ class QuickChat extends Disposable { uri: item.uri }); } + } else if (item.kind === 'notebookEditGroup') { + for (const group of item.edits) { + message.push({ + kind: 'notebookEdit', + edits: group, + uri: item.uri + }); + } } else { message.push(item); } diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts index 386758af35e..e761fc19358 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts @@ -8,9 +8,11 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; export interface ICodeMapperResponse { textEdit: (resource: URI, textEdit: TextEdit[]) => void; + notebookEdit: (resource: URI, edit: ICellEditOperation[]) => void; } export interface ICodeMapperCodeBlock { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 140282d5d42..93321b78ca3 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -17,6 +17,7 @@ import { localize } from '../../../../nls.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IChatResponseModel } from './chatModel.js'; +import { ICellEditReplaceOperation } from './chatService.js'; export const IChatEditingService = createDecorator('chatEditingService'); @@ -149,6 +150,7 @@ export interface IModifiedFileEntry { export interface IChatEditingSessionStream { textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void; + notebookEdits(resource: URI, edits: ICellEditReplaceOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void; } export const enum ChatEditingSessionState { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 4848dea4e2f..6021538f68e 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -23,7 +23,7 @@ import { localize } from '../../../../nls.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, IChatWelcomeMessageContent, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ICellEditReplaceOperation, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; export interface IBaseChatRequestVariableEntry { @@ -143,6 +143,14 @@ export interface IChatTextEditGroup { done: boolean | undefined; } +export interface IChatNotebookEditGroup { + uri: URI; + edits: ICellEditReplaceOperation[][]; + state?: IChatTextEditGroupState; + kind: 'notebookEditGroup'; + done: boolean | undefined; +} + /** * Progress kinds that are included in the history of a response. * Excludes "internal" types that are included in history. @@ -158,6 +166,7 @@ export type IChatProgressHistoryResponseContent = | IChatWarningMessage | IChatTask | IChatTextEditGroup + | IChatNotebookEditGroup | IChatConfirmation; /** @@ -376,6 +385,7 @@ class AbstractResponse implements IResponse { segment = { text: part.command.title, isBlock: true }; break; case 'textEditGroup': + case 'notebookEditGroup': segment = { text: localize('editsSummary', "Made changes."), isBlock: true }; break; case 'confirmation': @@ -459,7 +469,7 @@ export class Response extends AbstractResponse implements IDisposable { this._updateRepr(true); } - updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatTask | IChatUndoStop, quiet?: boolean): void { + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask | IChatUndoStop, quiet?: boolean): void { if (progress.kind === 'markdownContent') { // last response which is NOT a text edit group because we do want to support heterogenous streaming but not have @@ -497,7 +507,26 @@ export class Response extends AbstractResponse implements IDisposable { }); } this._updateRepr(quiet); - + } else if (progress.kind === 'notebookEdit') { + // merge text edits for the same file no matter when they come in + let found = false; + for (let i = 0; !found && i < this._responseParts.length; i++) { + const candidate = this._responseParts[i]; + if (candidate.kind === 'notebookEditGroup' && isEqual(candidate.uri, progress.uri)) { + candidate.edits.push(progress.edits); + candidate.done = progress.done; + found = true; + } + } + if (!found) { + this._responseParts.push({ + kind: 'notebookEditGroup', + uri: progress.uri, + edits: [progress.edits], + done: progress.done + }); + } + this._updateRepr(quiet); } else if (progress.kind === 'progressTask') { // Add a new resolving part const responsePosition = this._responseParts.push(progress) - 1; @@ -707,7 +736,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel /** * Apply a progress update to the actual response content. */ - updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit, quiet?: boolean) { + updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) { this.bufferWhenPaused(() => this._response.updateContent(responsePart, quiet)); } @@ -1427,6 +1456,7 @@ export class ChatModel extends Disposable implements IChatModel { progress.kind === 'progressMessage' || progress.kind === 'command' || progress.kind === 'textEdit' || + progress.kind === 'notebookEdit' || progress.kind === 'warning' || progress.kind === 'progressTask' || progress.kind === 'confirmation' || diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 3c645397279..b97df2dc013 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -14,6 +14,7 @@ import { ISelection } from '../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; import { FileType } from '../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { CellEditType, ICellDto2, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IWorkspaceSymbol } from '../../search/common/search.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; @@ -186,6 +187,19 @@ export interface IChatTextEdit { done?: boolean; } +export type ICellEditReplaceOperation = { + editType: CellEditType.Replace; + index: number; + count: number; + cells: ICellDto2[]; +}; +export interface IChatNotebookEdit { + uri: URI; + edits: ICellEditReplaceOperation[]; + kind: 'notebookEdit'; + done?: boolean; +} + export interface IChatConfirmation { title: string; message: string; @@ -237,6 +251,7 @@ export type IChatProgress = | IChatCommandButton | IChatWarningMessage | IChatTextEdit + | IChatNotebookEdit | IChatMoveMessage | IChatResponseCodeblockUriPart | IChatConfirmation @@ -479,3 +494,20 @@ export interface IChatService { } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; + +export namespace NotebookEdits { + export function toCellEditReplaceOperation(edits: ICellEditOperation[]): ICellEditReplaceOperation[] { + const result: ICellEditReplaceOperation[] = []; + for (const edit of edits) { + if (edit.editType === CellEditType.Replace) { + result.push({ + editType: CellEditType.Replace, + index: edit.index, + count: edit.count, + cells: edit.cells + }); + } + } + return result; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 6de2384da9b..fbbf64c6f45 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -16,7 +16,7 @@ import { ITextFileService } from '../../../../services/textfile/common/textfiles import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; import { IChatEditingService } from '../../common/chatEditingService.js'; import { ChatModel } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; +import { IChatService, NotebookEdits } from '../../common/chatService.js'; import { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../common/languageModelToolsService.js'; import { IToolInputProcessor } from './tools.js'; @@ -127,7 +127,10 @@ export class EditTool implements IToolImpl { }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); - } + }, + notebookEdit(target, edits) { + model.acceptResponseProgress(request, { kind: 'notebookEdit', uri: target, edits: NotebookEdits.toCellEditReplaceOperation(edits) }); + }, }, token); model.acceptResponseProgress(request, { kind: 'textEdit', uri, edits: [], done: true }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index 59607a12d42..e127d928202 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -446,6 +446,14 @@ export class TerminalChatWidget extends Disposable { uri: item.uri }); } + } else if (item.kind === 'notebookEditGroup') { + for (const group of item.edits) { + message.push({ + kind: 'notebookEdit', + edits: group, + uri: item.uri + }); + } } else { message.push(item); } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index ed5da524cea..5859111049a 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -55,6 +55,14 @@ declare module 'vscode' { constructor(uri: Uri, edits: TextEdit | TextEdit[]); } + export class ChatResponseNotebookEditPart { + uri: Uri; + edits: NotebookEdit[]; + isDone?: boolean; + constructor(uri: Uri, done: true); + constructor(uri: Uri, edits: NotebookEdit | NotebookEdit[]); + } + export class ChatResponseConfirmationPart { title: string; message: string; @@ -166,6 +174,10 @@ declare module 'vscode' { textEdit(target: Uri, isDone: true): void; + notebookEdit(target: Uri, edits: NotebookEdit | NotebookEdit[]): void; + + notebookEdit(target: Uri, isDone: true): void; + markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; codeblockUri(uri: Uri): void; push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; diff --git a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts index ac72b52ff37..50249caa8a9 100644 --- a/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts @@ -79,6 +79,7 @@ declare module 'vscode' { export interface MappedEditsResponseStream { textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; + notebookEdit(target: Uri, edits: NotebookEdit | NotebookEdit[]): void; } export interface MappedEditsResult {