diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index daa52e7d31f..c7ab0c27b05 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -43,7 +43,7 @@ import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/brow import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingSession, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { ChatEntitlement, ChatSentiment, IChatEntitlementService } from '../../common/chatEntitlementService.js'; import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; import { IChatDetail, IChatService } from '../../common/chatService.js'; @@ -241,7 +241,6 @@ export function registerChatActions() { const viewsService = accessor.get(IViewsService); const editorService = accessor.get(IEditorService); const dialogService = accessor.get(IDialogService); - const chatEditingService = accessor.get(IChatEditingService); const view = await viewsService.openView(ChatViewId); if (!view) { @@ -253,7 +252,7 @@ export function registerChatActions() { return; } - const editingSession = chatEditingService.getEditingSession(chatSessionId); + const editingSession = view.widget.viewModel?.model.editingSession; if (editingSession) { const phrase = localize('switchChat.confirmPhrase', "Switching chats will end your current edit session."); await handleCurrentEditingSession(editingSession, phrase, dialogService); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 2d2a1debb32..c0931610c8f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -505,7 +505,7 @@ class SendToChatEditingAction extends Action2 { if (!editingWidget.viewModel?.sessionId) { return; } - const chatEditingSession = await chatEditingService.startOrContinueGlobalEditingSession(editingWidget.viewModel.sessionId); + const chatEditingSession = editingWidget.viewModel.model.editingSession; if (!chatEditingSession) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 6694b8b3308..8fa805eaece 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -236,8 +236,7 @@ export function registerChatTitleActions() { if (chatModel?.initialLocation === ChatAgentLocation.EditingSession || chatModel && (mode === ChatMode.Edit || mode === ChatMode.Agent)) { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); - const chatEditingService = accessor.get(IChatEditingService); - const currentEditingSession = chatEditingService.getEditingSession(chatModel.sessionId); + const currentEditingSession = widget?.viewModel?.model.editingSession; if (!currentEditingSession) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index 4dcd020e1c7..f94b9348a32 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -32,9 +32,9 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILabelService } from '../../../../../platform/label/common/label.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IMarkdownVulnerability } from '../../common/annotations.js'; -import { IChatEditingService, IEditSessionEntryDiff } from '../../common/chatEditingService.js'; +import { IEditSessionEntryDiff } from '../../common/chatEditingService.js'; import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js'; -import { IChatMarkdownContent, IChatUndoStop } from '../../common/chatService.js'; +import { IChatMarkdownContent, IChatService, IChatUndoStop } from '../../common/chatService.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockEntry, CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; import { IChatCodeBlockInfo } from '../chat.js'; @@ -318,8 +318,8 @@ class CollapsedCodeBlock extends Disposable { @IContextMenuService private readonly contextMenuService: IContextMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IChatEditingService private readonly chatEditingService: IChatEditingService, @IHoverService private readonly hoverService: IHoverService, + @IChatService private readonly chatService: IChatService, ) { super(); this.element = $('.chat-codeblock-pill-widget'); @@ -355,58 +355,60 @@ class CollapsedCodeBlock extends Disposable { this._uri = uri; + const session = this.chatService.getSession(this.sessionId); const iconText = this.labelService.getUriBasenameLabel(uri); - const editSession = this.chatEditingService.getEditingSession(this.sessionId); - const modifiedEntry = editSession?.getEntry(uri); - const isComplete = !modifiedEntry?.isCurrentlyBeingModifiedBy.get(); - - let iconClasses: string[] = []; - if (isStreaming || !isComplete) { - const codicon = ThemeIcon.modify(Codicon.loading, 'spin'); - iconClasses = ThemeIcon.asClassNameArray(codicon); - } else { - const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; - iconClasses = getIconClasses(this.modelService, this.languageService, uri, fileKind); - } - - const iconEl = dom.$('span.icon'); - iconEl.classList.add(...iconClasses); - - const children = [dom.$('span.icon-label', {}, iconText)]; - const labelDetail = dom.$('span.label-detail', {}, ''); - children.push(labelDetail); - if (isStreaming) { - labelDetail.textContent = localize('chat.codeblock.generating', "Generating edits..."); - } - - this.element.replaceChildren(iconEl, ...children); - this.updateTooltip(this.labelService.getUriLabel(uri, { relative: false })); - - const renderDiff = (changes: IEditSessionEntryDiff | undefined) => { - const labelAdded = this.element.querySelector('.label-added') ?? this.element.appendChild(dom.$('span.label-added')); - const labelRemoved = this.element.querySelector('.label-removed') ?? this.element.appendChild(dom.$('span.label-removed')); - if (changes && !changes?.identical && !changes?.quitEarly) { - this._currentDiff = changes; - labelAdded.textContent = `+${changes.added}`; - labelRemoved.textContent = `-${changes.removed}`; - const insertionsFragment = changes.added === 1 ? localize('chat.codeblock.insertions.one', "1 insertion") : localize('chat.codeblock.insertions', "{0} insertions", changes.added); - const deletionsFragment = changes.removed === 1 ? localize('chat.codeblock.deletions.one', "1 deletion") : localize('chat.codeblock.deletions', "{0} deletions", changes.removed); - const summary = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment); - this.element.ariaLabel = summary; - this.updateTooltip(summary); - } - }; - - const diffBetweenStops = modifiedEntry && editSession - ? editSession.getEntryDiffBetweenStops(modifiedEntry.modifiedURI, this.requestId, this.inUndoStop) - : undefined; - - // Show a percentage progress that is driven by the rewrite - this._progressStore.add(autorun(r => { + const editSession = session?.editingSessionObs?.promiseResult.read(r)?.data; + const modifiedEntry = editSession?.getEntry(uri); + const modifiedByRequest = modifiedEntry?.isCurrentlyBeingModifiedBy.read(r); + const isComplete = !modifiedByRequest || modifiedByRequest.id !== this.requestId; + + let iconClasses: string[] = []; + if (isStreaming || !isComplete) { + const codicon = ThemeIcon.modify(Codicon.loading, 'spin'); + iconClasses = ThemeIcon.asClassNameArray(codicon); + } else { + const fileKind = uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE; + iconClasses = getIconClasses(this.modelService, this.languageService, uri, fileKind); + } + + const iconEl = dom.$('span.icon'); + iconEl.classList.add(...iconClasses); + + const children = [dom.$('span.icon-label', {}, iconText)]; + const labelDetail = dom.$('span.label-detail', {}, ''); + children.push(labelDetail); + if (isStreaming) { + labelDetail.textContent = localize('chat.codeblock.generating', "Generating edits..."); + } + + this.element.replaceChildren(iconEl, ...children); + this.updateTooltip(this.labelService.getUriLabel(uri, { relative: false })); + + const renderDiff = (changes: IEditSessionEntryDiff | undefined) => { + const labelAdded = this.element.querySelector('.label-added') ?? this.element.appendChild(dom.$('span.label-added')); + const labelRemoved = this.element.querySelector('.label-removed') ?? this.element.appendChild(dom.$('span.label-removed')); + if (changes && !changes?.identical && !changes?.quitEarly) { + this._currentDiff = changes; + labelAdded.textContent = `+${changes.added}`; + labelRemoved.textContent = `-${changes.removed}`; + const insertionsFragment = changes.added === 1 ? localize('chat.codeblock.insertions.one', "1 insertion") : localize('chat.codeblock.insertions', "{0} insertions", changes.added); + const deletionsFragment = changes.removed === 1 ? localize('chat.codeblock.deletions.one', "1 deletion") : localize('chat.codeblock.deletions', "{0} deletions", changes.removed); + const summary = localize('summary', 'Edited {0}, {1}, {2}', iconText, insertionsFragment, deletionsFragment); + this.element.ariaLabel = summary; + this.updateTooltip(summary); + } + }; + + const diffBetweenStops = modifiedEntry && editSession + ? editSession.getEntryDiffBetweenStops(modifiedEntry.modifiedURI, this.requestId, this.inUndoStop) + : undefined; + + // Show a percentage progress that is driven by the rewrite + + const rewriteRatio = modifiedEntry?.rewriteRatio.read(r); - const isComplete = !modifiedEntry?.isCurrentlyBeingModifiedBy.read(r); if (!isStreaming && !isComplete) { const value = rewriteRatio; labelDetail.textContent = value === 0 || !value ? localize('chat.codeblock.generating', "Generating edits...") : localize('chat.codeblock.applyingPercentage', "Applying edits ({0}%)...", Math.round(value * 100)); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 81acd879059..a5dfed97c8a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -476,14 +476,13 @@ registerAction2(class RemoveAction extends Action2 { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); - const chatEditingService = accessor.get(IChatEditingService); const chatService = accessor.get(IChatService); const chatModel = chatService.getSession(item.sessionId); if (!chatModel) { return; } - const session = chatEditingService.getEditingSession(chatModel.sessionId); + const session = chatModel.editingSession; if (!session) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index e3c9bfd2c82..e0a51955423 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -16,7 +16,7 @@ import { derived, IObservable, observableValueOpts, runOnChange, ValueWithChange import { isEqual } from '../../../../../base/common/resources.js'; import { compare } from '../../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { assertType, isString } from '../../../../../base/common/types.js'; +import { assertType } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; @@ -26,7 +26,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IProductService } from '../../../../../platform/product/common/productService.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IStorageService } from '../../../../../platform/storage/common/storage.js'; import { IDecorationData, IDecorationsProvider, IDecorationsService } from '../../../../services/decorations/common/decorations.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; @@ -36,16 +36,13 @@ import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; import { IChatAgentService } from '../../common/chatAgents.js'; import { CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingAgentSupportsReadonlyReferencesContextKey, chatEditingResourceContextKey, ChatEditingSessionState, chatEditingSnapshotScheme, IChatEditingService, IChatEditingSession, IChatRelatedFile, IChatRelatedFilesProvider, IModifiedFileEntry, inChatEditingSessionContextKey, IStreamingEdits, WorkingSetEntryState } from '../../common/chatEditingService.js'; -import { IChatResponseModel, isCellTextEditOperation } from '../../common/chatModel.js'; +import { ChatModel, IChatResponseModel, isCellTextEditOperation } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingSession } from './chatEditingSession.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; - -const STORAGE_KEY_EDITING_SESSION = 'chat.editingSession'; - export class ChatEditingService extends Disposable implements IChatEditingService { _serviceBrand: undefined; @@ -107,21 +104,15 @@ export class ChatEditingService extends Disposable implements IChatEditingServic let storageTask: Promise | undefined; this._register(storageService.onWillSaveState(() => { - const sessionIds: string[] = []; const tasks: Promise[] = []; for (const session of this.editingSessionsObs.get()) { if (!session.isGlobalEditingSession) { continue; } - sessionIds.push(session.chatSessionId); tasks.push((session as ChatEditingSession).storeState()); } - if (sessionIds.length) { - storageService.store(STORAGE_KEY_EDITING_SESSION, sessionIds.join(), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - storageTask = Promise.resolve(storageTask) .then(() => Promise.all(tasks)) .finally(() => storageTask = undefined); @@ -136,27 +127,6 @@ export class ChatEditingService extends Disposable implements IChatEditingServic label: localize('join.chatEditingSession', "Saving chat edits history") }); })); - - const rawSessionsToRestore = storageService.get(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); - if (isString(rawSessionsToRestore)) { - - const sessionIds = rawSessionsToRestore.split(','); - - const tasks = sessionIds.map(async sessionId => { - const chatModel = await _chatService.getOrRestoreSession(sessionId); - if (!chatModel) { - logService.error(`Edit session session to restore is a non-existing chat session: ${rawSessionsToRestore}`); - return; - } - await this.startOrContinueGlobalEditingSession(chatModel.sessionId, false); - }); - - this._restoringEditingSession = Promise.all(tasks).finally(() => { - this._restoringEditingSession = undefined; - }); - - storageService.remove(STORAGE_KEY_EDITING_SESSION, StorageScope.WORKSPACE); - } } override dispose(): void { @@ -164,16 +134,17 @@ export class ChatEditingService extends Disposable implements IChatEditingServic super.dispose(); } - async startOrContinueGlobalEditingSession(chatSessionId: string, waitForRestore = true): Promise { + async startOrContinueGlobalEditingSession(chatModel: ChatModel, waitForRestore = true): Promise { if (waitForRestore) { await this._restoringEditingSession; } - const session = this.getEditingSession(chatSessionId); + const session = this.getEditingSession(chatModel.sessionId); if (session) { return session; } - return this.createEditingSession(chatSessionId, true); + const result = await this.createEditingSession(chatModel, true); + return result; } @@ -194,11 +165,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic .find(candidate => candidate.chatSessionId === chatSessionId); } - async createEditingSession(chatSessionId: string, global: boolean = false): Promise { + async createEditingSession(chatModel: ChatModel, global: boolean = false): Promise { - assertType(this.getEditingSession(chatSessionId) === undefined, 'CANNOT have more than one editing session per chat session'); + assertType(this.getEditingSession(chatModel.sessionId) === undefined, 'CANNOT have more than one editing session per chat session'); - const session = this._instantiationService.createInstance(ChatEditingSession, chatSessionId, global, this._lookupEntry.bind(this)); + const session = this._instantiationService.createInstance(ChatEditingSession, chatModel.sessionId, global, this._lookupEntry.bind(this)); await session.init(); const list = this._sessionsObs.get(); @@ -207,7 +178,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic const store = new DisposableStore(); this._store.add(store); - store.add(await this.installAutoApplyObserver(session)); + store.add(this.installAutoApplyObserver(session, chatModel)); store.add(session.onDidDispose(e => { removeSession(); @@ -220,8 +191,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return session; } - private async installAutoApplyObserver(session: ChatEditingSession): Promise { - const chatModel = await this._chatService.getOrRestoreSession(session.chatSessionId); + private installAutoApplyObserver(session: ChatEditingSession, chatModel: ChatModel): IDisposable { if (!chatModel) { throw new ErrorNoTelemetry(`Edit session was created for a non-existing chat session: ${session.chatSessionId}`); } diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 83f29282e80..2472469bcd1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -355,24 +355,6 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderChatEditingSessionState(); })); - if (this._location.location === ChatAgentLocation.EditingSession || (this.chatService.unifiedViewEnabled && this._location.location !== ChatAgentLocation.Editor)) { - let currentEditSession: IChatEditingSession | undefined = undefined; - this._register(this.onDidChangeViewModel(async () => { - const sessionId = this._viewModel?.sessionId; - if (sessionId) { - if (sessionId !== currentEditSession?.chatSessionId) { - currentEditSession = await chatEditingService.startOrContinueGlobalEditingSession(sessionId); - } - } else { - if (currentEditSession) { - const session = currentEditSession; - currentEditSession = undefined; - await session.stop(); - } - } - })); - } - this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { const resource = input.resource; if (resource.scheme !== Schemas.vscodeChatCodeBlock) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 5755bac4e0c..21e005411e9 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -692,7 +692,7 @@ class BuiltinDynamicCompletions extends Disposable { const len = result.suggestions.length; // RELATED FILES - if (widget.input.currentMode !== ChatMode.Ask && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) { + if (widget.input.currentMode !== ChatMode.Ask && widget.viewModel && widget.viewModel.model.editingSession) { const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 0de480c9d88..629bbd1cdc7 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -14,7 +14,7 @@ import { RawContextKey } from '../../../../platform/contextkey/common/contextkey import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorPane } from '../../../common/editor.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { IChatResponseModel } from './chatModel.js'; +import { ChatModel, IChatResponseModel } from './chatModel.js'; export const IChatEditingService = createDecorator('chatEditingService'); @@ -22,7 +22,7 @@ export interface IChatEditingService { _serviceBrand: undefined; - startOrContinueGlobalEditingSession(chatSessionId: string): Promise; + startOrContinueGlobalEditingSession(chatModel: ChatModel): Promise; getEditingSession(chatSessionId: string): IChatEditingSession | undefined; @@ -34,7 +34,7 @@ export interface IChatEditingService { /** * Creates a new short lived editing session */ - createEditingSession(chatSessionId: string): Promise; + createEditingSession(chatModel: ChatModel): Promise; //#region related files diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 870ff0d48a2..dfae1272428 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -12,7 +12,7 @@ import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; -import { IObservable, ITransaction, observableValue } from '../../../../base/common/observable.js'; +import { IObservable, ITransaction, ObservablePromise, observableValue } from '../../../../base/common/observable.js'; import { basename, isEqual } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js'; @@ -21,14 +21,16 @@ import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offset import { IRange } from '../../../../editor/common/core/range.js'; import { Location, SymbolKind, TextEdit } from '../../../../editor/common/languages.js'; import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IMarker, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; +import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, 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'; -import { ChatAgentLocation } from './constants.js'; +import { ChatAgentLocation, ChatConfiguration } from './constants.js'; export interface IBaseChatRequestVariableEntry { id: string; @@ -944,6 +946,8 @@ export interface IChatModel { readonly requestInProgress: boolean; readonly requestPausibility: ChatPauseState; readonly inputPlaceholder?: string; + readonly editingSessionObs?: ObservablePromise | undefined; + readonly editingSession?: IChatEditingSession | undefined; toggleLastRequestPaused(paused?: boolean): void; /** * Sets requests as 'disabled', removing them from the UI. If a request ID @@ -1299,11 +1303,22 @@ export class ChatModel extends Disposable implements IChatModel { return this._initialLocation; } + private _editingSession: ObservablePromise | undefined; + get editingSessionObs(): ObservablePromise | undefined { + return this._editingSession; + } + + get editingSession(): IChatEditingSession | undefined { + return this._editingSession?.promiseResult.get()?.data; + } + constructor( private readonly initialData: ISerializableChatData | IExportableChatData | undefined, private readonly _initialLocation: ChatAgentLocation, @ILogService private readonly logService: ILogService, @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatEditingService chatEditingService: IChatEditingService, + @IConfigurationService configurationService: IConfigurationService, ) { super(); @@ -1321,6 +1336,11 @@ export class ChatModel extends Disposable implements IChatModel { this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; + + if (this._initialLocation === ChatAgentLocation.EditingSession || (configurationService.getValue(ChatConfiguration.UnifiedChatView) && this._initialLocation === ChatAgentLocation.Panel)) { + this._editingSession = new ObservablePromise(chatEditingService.startOrContinueGlobalEditingSession(this)); + this._editingSession.promise.then(editingSession => this._register(editingSession)); + } } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts index 886858d3fdf..96198ee1877 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts @@ -18,7 +18,6 @@ import { ITextFileService } from '../../../../services/textfile/common/textfiles import { CellUri } from '../../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; 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 { ILanguageModelIgnoredFilesService } from '../../common/ignoredFiles.js'; @@ -75,7 +74,6 @@ export class EditTool implements IToolImpl { constructor( @IChatService private readonly chatService: IChatService, - @IChatEditingService private readonly chatEditingService: IChatEditingService, @ICodeMapperService private readonly codeMapperService: ICodeMapperService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @ILanguageModelIgnoredFilesService private readonly ignoredFilesService: ILanguageModelIgnoredFilesService, @@ -151,7 +149,7 @@ export class EditTool implements IToolImpl { }); } - const editSession = this.chatEditingService.getEditingSession(model.sessionId); + const editSession = model.editingSession; if (!editSession) { throw new Error('This tool must be called from within an editing session'); } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts index 17490eb40ef..682d72460b1 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts @@ -115,14 +115,14 @@ suite('ChatEditingService', function () { assert.ok(editingService); const model = chatService.startSession(ChatAgentLocation.EditingSession, CancellationToken.None); - const session = await editingService.createEditingSession(model.sessionId, true); + const session = await editingService.createEditingSession(model, true); assert.strictEqual(session.chatSessionId, model.sessionId); assert.strictEqual(session.isGlobalEditingSession, true); await assertThrowsAsync(async () => { // DUPE not allowed - await editingService.createEditingSession(model.sessionId); + await editingService.createEditingSession(model); }); session.dispose(); @@ -136,7 +136,7 @@ suite('ChatEditingService', function () { const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); const model = chatService.startSession(ChatAgentLocation.EditingSession, CancellationToken.None); - const session = await editingService.createEditingSession(model.sessionId, true); + const session = await editingService.createEditingSession(model, true); const chatRequest = model?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); assertType(chatRequest.response); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts index cce4f5c0b98..768f7892ded 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts @@ -22,6 +22,8 @@ import { ChatRequestTextPart } from '../../common/chatParserTypes.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { ChatAgentLocation } from '../../common/constants.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; suite('ChatModel', () => { const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -35,6 +37,7 @@ suite('ChatModel', () => { instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); }); test('Waits for initialization', async () => { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 7c349948126..d56ff17b996 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -1424,7 +1424,7 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito const uri = editor.getModel().uri; const chatModel = chatService.startSession(ChatAgentLocation.Editor, token); - const editSession = await chatEditingService.createEditingSession(chatModel.sessionId); + const editSession = await chatEditingService.createEditingSession(chatModel); const store = new DisposableStore(); store.add(chatModel); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index a503d632eea..599195d2fcd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -340,7 +340,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const chatModel = this._chatService.startSession(ChatAgentLocation.EditingSession, token); - const editingSession = await this._chatEditingService.createEditingSession(chatModel.sessionId); + const editingSession = await this._chatEditingService.createEditingSession(chatModel); const widget = this._chatWidgetService.getWidgetBySessionId(chatModel.sessionId); widget?.attachmentModel.addFile(uri);