diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 9006925683d..37c4dbb7403 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -60,7 +60,6 @@ import { registerChatEditorActions } from './chatEditorActions.js'; import { ChatEditorController } from './chatEditorController.js'; import { ChatEditorInput, ChatEditorInputSerializer } from './chatEditorInput.js'; import { ChatInputBoxContentProvider } from './chatEdinputInputContentProvider.js'; -import { ChatEditorAutoSaveDisabler, ChatEditorSaving } from './chatEditorSaving.js'; import { agentSlashCommandToMarkdown, agentToMarkdown } from './chatMarkdownDecorationsRenderer.js'; import { ChatCompatibilityNotifier, ChatExtensionPointHandler } from './chatParticipant.contribution.js'; import { ChatPasteProvidersFeature } from './chatPasteProviders.js'; @@ -122,12 +121,6 @@ configurationRegistry.registerConfiguration({ markdownDescription: nls.localize('chat.commandCenter.enabled', "Controls whether the command center shows a menu for actions to control Copilot (requires {0}).", '`#window.commandCenter#`'), default: true }, - 'chat.editing.alwaysSaveWithGeneratedChanges': { - type: 'boolean', - scope: ConfigurationScope.APPLICATION, - markdownDescription: nls.localize('chat.editing.alwaysSaveWithGeneratedChanges', "Whether files that have changes made by chat can be saved without confirmation."), - default: false, - }, 'chat.editing.autoAcceptDelay': { type: 'number', markdownDescription: nls.localize('chat.editing.autoAcceptDelay', "Delay after which changes made by chat are automatically accepted. Values are in seconds, `0` means disabled and `100` seconds is the maximum."), @@ -319,8 +312,6 @@ registerWorkbenchContribution2(ChatCompatibilityNotifier.ID, ChatCompatibilityNo registerWorkbenchContribution2(ChatCommandCenterRendering.ID, ChatCommandCenterRendering, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatImplicitContextContribution.ID, ChatImplicitContextContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatRelatedFilesContribution.ID, ChatRelatedFilesContribution, WorkbenchPhase.Eventually); -registerWorkbenchContribution2(ChatEditorSaving.ID, ChatEditorSaving, WorkbenchPhase.AfterRestored); -registerWorkbenchContribution2(ChatEditorAutoSaveDisabler.ID, ChatEditorAutoSaveDisabler, WorkbenchPhase.BlockRestore); registerWorkbenchContribution2(ChatViewsWelcomeHandler.ID, ChatViewsWelcomeHandler, WorkbenchPhase.BlockStartup); registerWorkbenchContribution2(ChatGettingStartedContribution.ID, ChatGettingStartedContribution, WorkbenchPhase.Eventually); registerWorkbenchContribution2(ChatSetupContribution.ID, ChatSetupContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts deleted file mode 100644 index d93c639d9d1..00000000000 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ /dev/null @@ -1,408 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { CancellationError } from '../../../../base/common/errors.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../base/common/map.js'; -import { autorun, autorunWithStore } from '../../../../base/common/observable.js'; -import { assertType } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { localize } from '../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IEditorIdentifier, SaveReason } from '../../../common/editor.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { AutoSaveMode, IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatModel } from '../common/chatModel.js'; -import { IChatService } from '../common/chatService.js'; -import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; - - -const STORAGE_KEY_AUTOSAVE_DISABLED = 'chat.editing.autosaveDisabled'; - -export class ChatEditorAutoSaveDisabler extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.autoSaveDisabler'; - - private _autosaveDisabledUris: string[] = []; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ILifecycleService lifecycleService: ILifecycleService, - @IStorageService storageService: IStorageService - ) { - super(); - - // on shutdown remember all files that have auto save disabled - this._store.add(lifecycleService.onWillShutdown((e) => { - storageService.store(STORAGE_KEY_AUTOSAVE_DISABLED, this._autosaveDisabledUris, StorageScope.WORKSPACE, StorageTarget.MACHINE); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - - // as quickly as possible disable auto save for all files that were modified before the last shutdown - if (!alwaysSaveConfig.get()) { - const autoSaveDisabled = storageService.getObject(STORAGE_KEY_AUTOSAVE_DISABLED, StorageScope.WORKSPACE, []); - if (Array.isArray(autoSaveDisabled) && autoSaveDisabled.length > 0) { - - const initializingStore = new DisposableStore(); - for (const uriString of autoSaveDisabled) { - initializingStore.add(fileConfigService.disableAutoSave(URI.parse(uriString))); - } - chatEditingService.getOrRestoreEditingSession().finally(() => { - // by now the session is restored and the auto save handlers are in place - initializingStore.dispose(); - }); - - } - } - - // listen to session changes and update auto save settings accordingly - const saveConfig = this._store.add(new MutableDisposable()); - this._store.add(autorun(reader => { - const store = new DisposableStore(); - const autoSaveDisabled: string[] = []; - try { - if (alwaysSaveConfig.read(reader)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(reader); - if (session) { - const entries = session.entries.read(reader); - for (const entry of entries) { - if (entry.state.read(reader) === WorkingSetEntryState.Modified) { - autoSaveDisabled.push(entry.modifiedURI.toString()); - store.add(fileConfigService.disableAutoSave(entry.modifiedURI)); - } - } - } - } finally { - saveConfig.value = store; // disposes the previous store, after we have added the new one - this._autosaveDisabledUris = autoSaveDisabled; - } - })); - } -} - - -export class ChatEditorSaving extends Disposable implements IWorkbenchContribution { - - static readonly ID: string = 'workbench.chat.editorSaving'; - - static readonly _config = 'chat.editing.alwaysSaveWithGeneratedChanges'; - - constructor( - @IConfigurationService configService: IConfigurationService, - @IChatEditingService chatEditingService: IChatEditingService, - @IChatAgentService chatAgentService: IChatAgentService, - @IFilesConfigurationService fileConfigService: IFilesConfigurationService, - @ITextFileService textFileService: ITextFileService, - @ILabelService labelService: ILabelService, - @IDialogService dialogService: IDialogService, - @IChatService private readonly _chatService: IChatService, - ) { - super(); - - // --- report that save happened - this._store.add(autorunWithStore((r, store) => { - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - const chatSession = this._chatService.getSession(session.chatSessionId); - if (!chatSession) { - return; - } - store.add(textFileService.files.onDidSave(e => { - const entry = session.getEntry(e.model.resource); - if (entry && entry.state.get() === WorkingSetEntryState.Modified) { - this._reportSavedWhenReady(chatSession, entry); - } - })); - })); - - const alwaysSaveConfig = observableConfigValue(ChatEditorSaving._config, false, configService); - this._store.add(autorunWithStore((r, store) => { - - const alwaysSave = alwaysSaveConfig.read(r); - - if (alwaysSave) { - return; - } - - const saveJobs = new class { - - private _deferred?: DeferredPromise; - private readonly _soon = new RunOnceScheduler(() => this._prompt(), 0); - private readonly _uris = new ResourceSet(); - - add(uri: URI) { - this._uris.add(uri); - this._soon.schedule(); - this._deferred ??= new DeferredPromise(); - return this._deferred.p; - } - - private async _prompt() { - - // this might have changed in the meantime and there is checked again and acted upon - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (alwaysSave) { - return; - } - - const uri = Iterable.first(this._uris); - if (!uri) { - // bogous? - return; - } - - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName ?? localize('chat', "chat"); - const filelabel = labelService.getUriBasenameLabel(uri); - - const message = this._uris.size === 1 - ? localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.2', "Do you want to save the changes {0} made to {1} files?", agentName, this._uris.size); - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - this._uris.clear(); - - if (result.confirmed && result.checkboxChecked) { - // remember choice - await configService.updateValue(ChatEditorSaving._config, true); - } - - if (!result.confirmed) { - // cancel the save - this._deferred?.error(new CancellationError()); - } else { - this._deferred?.complete(); - } - this._deferred = undefined; - } - }; - - store.add(textFileService.files.addSaveParticipant({ - participate: async (workingCopy, context, progress, token) => { - - if (context.reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - const session = await chatEditingService.getOrRestoreEditingSession(); - if (!session || session.isToolsAgentSession) { - // For now, don't prompt when in agent mode - return; - } - const entry = session.getEntry(workingCopy.resource); - if (!entry || entry.state.get() !== WorkingSetEntryState.Modified) { - return; - } - - return saveJobs.add(entry.modifiedURI); - } - })); - })); - - // autosave: OFF & alwaysSaveWithAIChanges - save files after accept - this._store.add(autorun(r => { - const saveConfig = fileConfigService.getAutoSaveMode(undefined); - if (saveConfig.mode !== AutoSaveMode.OFF) { - return; - } - if (!alwaysSaveConfig.read(r)) { - return; - } - const session = chatEditingService.currentEditingSessionObs.read(r); - if (!session) { - return; - } - for (const entry of session.entries.read(r)) { - if (entry.state.read(r) === WorkingSetEntryState.Accepted) { - textFileService.save(entry.modifiedURI); - } - } - })); - } - - private _reportSaved(entry: IModifiedFileEntry) { - assertType(entry instanceof ChatEditingModifiedFileEntry); - - this._chatService.notifyUserAction({ - action: { kind: 'chatEditingSessionAction', uri: entry.modifiedURI, hasRemainingEdits: false, outcome: 'saved' }, - agentId: entry.telemetryInfo.agentId, - command: entry.telemetryInfo.command, - sessionId: entry.telemetryInfo.sessionId, - requestId: entry.telemetryInfo.requestId, - result: entry.telemetryInfo.result - }); - } - - private _reportSavedWhenReady(session: IChatModel, entry: IModifiedFileEntry) { - if (!session.requestInProgress) { - this._reportSaved(entry); - return; - } - // wait until no more request is pending - const d = session.onDidChange(e => { - if (!session.requestInProgress) { - this._reportSaved(entry); - this._store.delete(d); - d.dispose(); - } - }); - this._store.add(d); - } -} - -export class ChatEditingSaveAllAction extends Action2 { - static readonly ID = 'chatEditing.saveAllFiles'; - - constructor() { - super({ - id: ChatEditingSaveAllAction.ID, - title: localize('save.allFiles', 'Save All'), - precondition: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey), - icon: Codicon.saveAll, - menu: [ - { - when: ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), - id: MenuId.EditorTitle, - order: 2, - group: 'navigation', - }, - { - id: MenuId.ChatEditingWidgetToolbar, - group: 'navigation', - order: 2, - // Show the option to save without accepting if the user hasn't configured the setting to always save with generated changes - when: ContextKeyExpr.and( - applyingChatEditsFailedContextKey.negate(), - hasUndecidedChatEditingResourceContextKey, - ContextKeyExpr.equals(`config.${ChatEditorSaving._config}`, false), - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) - ) - } - ], - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.KeyS, - when: ContextKeyExpr.and(ChatContextKeys.requestInProgress.negate(), hasUndecidedChatEditingResourceContextKey, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput), - weight: KeybindingWeight.WorkbenchContrib, - }, - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - const editorService = accessor.get(IEditorService); - const configService = accessor.get(IConfigurationService); - const chatAgentService = accessor.get(IChatAgentService); - const dialogService = accessor.get(IDialogService); - const labelService = accessor.get(ILabelService); - - const currentEditingSession = chatEditingService.currentEditingSession; - if (!currentEditingSession) { - return; - } - - const editors: IEditorIdentifier[] = []; - for (const modifiedFileEntry of currentEditingSession.entries.get()) { - if (modifiedFileEntry.state.get() === WorkingSetEntryState.Modified) { - const modifiedFile = modifiedFileEntry.modifiedURI; - const matchingEditors = editorService.findEditors(modifiedFile); - if (matchingEditors.length === 0) { - continue; - } - const matchingEditor = matchingEditors[0]; - if (matchingEditor.editor.isDirty()) { - editors.push(matchingEditor); - } - } - } - - if (editors.length === 0) { - return; - } - - const alwaysSave = configService.getValue(ChatEditorSaving._config); - if (!alwaysSave) { - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditingSession)?.fullName; - - let message: string; - if (editors.length === 1) { - const resource = editors[0].editor.resource; - if (resource) { - const filelabel = labelService.getUriBasenameLabel(resource); - message = agentName - ? localize('message.batched.oneFile.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel) - : localize('message.batched.oneFile.2', "Do you want to save the changes chat made in {0}?", filelabel); - } else { - message = agentName - ? localize('message.batched.oneFile.3', "Do you want to save the changes {0} made in 1 file?", agentName) - : localize('message.batched.oneFile.4', "Do you want to save the changes chat made in 1 file?"); - } - } else { - message = agentName - ? localize('message.batched.multiFile.1', "Do you want to save the changes {0} made in {1} files?", agentName, editors.length) - : localize('message.batched.multiFile.2', "Do you want to save the changes chat made in {0} files?", editors.length); - } - - - const result = await dialogService.confirm({ - message, - detail: localize('detail2', "AI-generated changes may be incorrect and should be reviewed before saving.", agentName), - primaryButton: localize('save all', "Save All"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - return; - } - - if (result.checkboxChecked) { - await configService.updateValue(ChatEditorSaving._config, true); - } - } - - // Skip our own chat editing save blocking participant, since we already showed our own batched dialog - await editorService.save(editors, { reason: SaveReason.EXPLICIT, skipSaveParticipants: true }); - } -} -registerAction2(ChatEditingSaveAllAction); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 0585b3012a4..8a676d3e323 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -99,7 +99,6 @@ import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatDragAndDrop, EditsDragAndDrop } from './chatDragAndDrop.js'; import { ChatEditingRemoveAllFilesAction, ChatEditingShowChangesAction } from './chatEditing/chatEditingActions.js'; -import { ChatEditingSaveAllAction } from './chatEditorSaving.js'; import { ChatFollowups } from './chatFollowups.js'; import { IChatViewState } from './chatWidget.js'; import { ChatFileReference } from './contrib/chatDynamicVariables/chatFileReference.js'; @@ -1310,7 +1309,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge arg: { sessionId: chatEditingSession.chatSessionId }, }, buttonConfigProvider: (action) => { - if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingSaveAllAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ChatEditingRemoveAllFilesAction.ID) { return { showIcon: true, showLabel: false, isSecondary: true }; } return undefined; diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 3572a0801b3..ee316323294 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -13,9 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { InlineChatSavingServiceImpl } from './inlineChatSavingServiceImpl.js'; import { InlineChatAccessibleView } from './inlineChatAccessibleView.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; @@ -30,7 +28,6 @@ import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHi // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index cbd575f7fde..6d8f16a122c 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -46,7 +46,6 @@ import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGr import { IChatService } from '../../chat/common/chatService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatError } from './inlineChatSessionServiceImpl.js'; @@ -139,7 +138,6 @@ export class InlineChatController implements IEditorContribution { private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService, @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @ILogService private readonly _logService: ILogService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -964,7 +962,6 @@ export class InlineChatController implements IEditorContribution { stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, }; - this._inlineChatSavingService.markChanged(this._session); if (opts) { await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); } else { @@ -1127,9 +1124,6 @@ export class InlineChatController implements IEditorContribution { unstashLastSession(): Session | undefined { const result = this._stashedSession.value?.unstash(); - if (result) { - this._inlineChatSavingService.markChanged(result); - } return result; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts deleted file mode 100644 index ebe92f44f05..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingService.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Session } from './inlineChatSession.js'; - - -export const IInlineChatSavingService = createDecorator('IInlineChatSavingService '); - -export interface IInlineChatSavingService { - _serviceBrand: undefined; - - markChanged(session: Session): void; - -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts deleted file mode 100644 index 53ae97f2806..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSavingServiceImpl.ts +++ /dev/null @@ -1,197 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Queue } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { DisposableStore, MutableDisposable, combinedDisposable, dispose } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../common/editor.js'; -import { Session } from './inlineChatSession.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { InlineChatConfigKeys } from '../common/inlineChat.js'; -import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; -import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IInlineChatSavingService } from './inlineChatSavingService.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { CellUri } from '../../notebook/common/notebookCommon.js'; -import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { CancellationError } from '../../../../base/common/errors.js'; - -interface SessionData { - readonly resourceUri: URI; - readonly dispose: () => void; - readonly session: Session; - readonly groupCandidate: IEditorGroup; -} - -// TODO@jrieken this duplicates a config key -const key = 'chat.editing.alwaysSaveWithGeneratedChanges'; - -export class InlineChatSavingServiceImpl implements IInlineChatSavingService { - - declare readonly _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - private readonly _saveParticipant = this._store.add(new MutableDisposable()); - private readonly _sessionData = new Map(); - - constructor( - @IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInlineChatSessionService _inlineChatSessionService: IInlineChatSessionService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, - @IDialogService private readonly _dialogService: IDialogService, - @ILabelService private readonly _labelService: ILabelService, - ) { - this._store.add(Event.any(_inlineChatSessionService.onDidEndSession, _inlineChatSessionService.onDidStashSession)(e => { - this._sessionData.get(e.session)?.dispose(); - })); - - this._store.add(_configService.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration(key) && !e.affectsConfiguration(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave)) { - return; - } - if (this._isDisabled()) { - dispose(this._sessionData.values()); - this._sessionData.clear(); - } - })); - } - - dispose(): void { - this._store.dispose(); - dispose(this._sessionData.values()); - } - - markChanged(session: Session): void { - - if (this._isDisabled()) { - return; - } - - if (!this._sessionData.has(session)) { - - let uri = session.targetUri; - - // notebooks: use the notebook-uri because saving happens on the notebook-level - if (uri.scheme === Schemas.vscodeNotebookCell) { - const data = CellUri.parse(uri); - if (!data) { - return; - } - uri = data?.notebook; - } - - if (this._sessionData.size === 0) { - this._installSaveParticpant(); - } - - const saveConfigOverride = this._fileConfigService.disableAutoSave(uri); - this._sessionData.set(session, { - resourceUri: uri, - groupCandidate: this._editorGroupService.activeGroup, - session, - dispose: () => { - saveConfigOverride.dispose(); - this._sessionData.delete(session); - if (this._sessionData.size === 0) { - this._saveParticipant.clear(); - } - } - }); - } - } - - private _installSaveParticpant(): void { - - const queue = new Queue(); - - const d1 = this._textFileService.files.addSaveParticipant({ - participate: (model, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? model.textEditorModel?.uri, ctx.reason, progress, token)); - } - }); - const d2 = this._workingCopyFileService.addSaveParticipant({ - participate: (workingCopy, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? workingCopy.resource, ctx.reason, progress, token)); - } - }); - this._saveParticipant.value = combinedDisposable(d1, d2, queue); - } - - private async _participate(uri: URI | undefined, reason: SaveReason, progress: IProgress, token: CancellationToken): Promise { - - - if (reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - if (this._isDisabled()) { - // disabled - return; - } - - const sessions = new Map(); - for (const [session, data] of this._sessionData) { - if (uri?.toString() === data.resourceUri.toString()) { - sessions.set(session, data); - } - } - - if (sessions.size === 0) { - return; - } - - let message: string; - - if (sessions.size === 1) { - const session = Iterable.first(sessions.values())!.session; - const agentName = session.agent.fullName; - const filelabel = this._labelService.getUriBasenameLabel(session.textModelN.uri); - - message = localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel); - } else { - const labels = Array.from(Iterable.map(sessions.values(), i => this._labelService.getUriBasenameLabel(i.session.textModelN.uri))); - message = localize('message.2', "Do you want to save the changes inline chat made in {0}?", labels.join(', ')); - } - - const result = await this._dialogService.confirm({ - message, - detail: localize('detail', "AI-generated changes may be incorrect and should be reviewed before saving."), - primaryButton: localize('save', "Save"), - cancelButton: localize('discard', "Cancel"), - checkbox: { - label: localize('config', "Always save with AI-generated changes without asking"), - checked: false - } - }); - - if (!result.confirmed) { - // cancel the save - throw new CancellationError(); - } - - if (result.checkboxChecked) { - // remember choice - this._configService.updateValue(key, true); - } - } - - private _isDisabled() { - return this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave) === true || this._configService.getValue(key); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 19f27bf3ad6..2270a8f9ef2 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -14,7 +14,6 @@ import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', AccessibleDiffView = 'inlineChat.accessibleDiffView', diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts index aaeb5de7b5a..06791cc9432 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts @@ -34,7 +34,6 @@ import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../. import { ChatAgentLocation, ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; import { InlineChatController, State } from '../../browser/inlineChatController.js'; -import { Session } from '../../browser/inlineChatSession.js'; import { CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; @@ -61,7 +60,6 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { assertType } from '../../../../../base/common/types.js'; import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { TestWorkerService } from './testWorkerService.js'; @@ -167,11 +165,6 @@ suite('InlineChatController', function () { [IChatEditingService, new class extends mock() { override currentEditingSessionObs: IObservable = observableValue(this, null); }], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } - }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { return { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts index 880552dd7ba..80b877eb95a 100644 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts @@ -26,8 +26,7 @@ import { IViewDescriptorService } from '../../../../common/views.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IChatAccessibilityService, IChatWidgetService } from '../../../chat/browser/chat.js'; import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; -import { IInlineChatSavingService } from '../../browser/inlineChatSavingService.js'; -import { HunkState, Session } from '../../browser/inlineChatSession.js'; +import { HunkState } from '../../browser/inlineChatSession.js'; import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; @@ -95,11 +94,6 @@ suite('InlineChatSession', function () { [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], [ICommandService, new SyncDescriptor(TestCommandService)], [ILanguageModelToolsService, new MockLanguageModelToolsService()], - [IInlineChatSavingService, new class extends mock() { - override markChanged(session: Session): void { - // noop - } - }], [IEditorProgressService, new class extends mock() { override show(total: unknown, delay?: unknown): IProgressRunner { return {