From 9cf56d4ac7e6c0d3b9da39e7f0e4b3ad18e7e75b Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Wed, 19 Nov 2025 11:11:47 -0800 Subject: [PATCH] chat: reduce mutability in ichatmodel (#278239) refs https://github.com/microsoft/vscode/issues/277318 --- .../server/node/serverEnvironmentService.ts | 2 + .../browser/actions/chatContinueInAction.ts | 28 ++++---- .../browser/chatEditing/chatEditingSession.ts | 17 ++--- .../contrib/chat/browser/chatEditor.ts | 6 +- .../contrib/chat/browser/chatWidget.ts | 2 +- .../chat/browser/languageModelToolsService.ts | 2 +- .../contrib/chat/common/chatEditingService.ts | 5 +- .../contrib/chat/common/chatModel.ts | 71 +++++++++---------- .../contrib/chat/common/chatService.ts | 5 ++ .../contrib/chat/common/chatServiceImpl.ts | 27 +++++-- .../browser/languageModelToolsService.test.ts | 9 ++- .../chat/test/common/chatService.test.ts | 7 +- .../chat/test/common/mockChatService.ts | 8 ++- .../browser/inlineChatController.ts | 2 +- 14 files changed, 109 insertions(+), 82 deletions(-) diff --git a/src/vs/server/node/serverEnvironmentService.ts b/src/vs/server/node/serverEnvironmentService.ts index ab7659a7ca5..ef423dc80fb 100644 --- a/src/vs/server/node/serverEnvironmentService.ts +++ b/src/vs/server/node/serverEnvironmentService.ts @@ -83,6 +83,7 @@ export const serverOptions: OptionDescriptions> = { 'enable-remote-auto-shutdown': { type: 'boolean' }, 'remote-auto-shutdown-without-delay': { type: 'boolean' }, + 'inspect-ptyhost': { type: 'string', allowEmptyValue: true }, 'use-host-proxy': { type: 'boolean' }, 'without-browser-env-var': { type: 'boolean' }, @@ -212,6 +213,7 @@ export interface ServerParsedArgs { 'enable-remote-auto-shutdown'?: boolean; 'remote-auto-shutdown-without-delay'?: boolean; + 'inspect-ptyhost'?: string; 'use-host-proxy'?: boolean; 'without-browser-env-var'?: boolean; diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts index 4b02e2e1f00..d8f7ae0669c 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts @@ -3,31 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action2, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; -import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; -import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; -import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { Codicon } from '../../../../../base/common/codicons.js'; -import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; -import { localize, localize2 } from '../../../../../nls.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { basename } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { isITextModel } from '../../../../../editor/common/model.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { Action2, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; +import { ChatModel } from '../../common/chatModel.js'; import { ChatRequestParser } from '../../common/chatRequestParser.js'; import { IChatService } from '../../common/chatService.js'; +import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; import { ChatAgentLocation } from '../../common/constants.js'; +import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js'; import { IChatWidgetService } from '../chat.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { CHAT_SETUP_ACTION_ID } from './chatActions.js'; export class ContinueChatInSessionAction extends Action2 { @@ -159,7 +160,8 @@ class CreateRemoteAgentJobAction { if (!widget.viewModel) { return; } - const chatModel = widget.viewModel.model; + // todo@connor4312: remove 'as' cast + const chatModel = widget.viewModel.model as ChatModel; if (!chatModel) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 50ab8f29ae0..4c0ac4d1bf1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -12,7 +12,7 @@ import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../../base/common/iterator.js'; import { Disposable, DisposableStore, dispose } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; -import { autorun, derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { derived, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; import { isEqual } from '../../../../../base/common/resources.js'; import { hasKey, Mutable } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; @@ -39,7 +39,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo import { INotebookService } from '../../../notebook/common/notebookService.js'; import { chatEditingSessionIsReady, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; -import { IChatProgress, IChatService } from '../../common/chatService.js'; +import { IChatProgress } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { IChatEditingCheckpointTimeline } from './chatEditingCheckpointTimeline.js'; import { ChatEditingCheckpointTimelineImpl, IChatEditingTimelineFsDelegate } from './chatEditingCheckpointTimelineImpl.js'; @@ -165,6 +165,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio public readonly canUndo: IObservable; public readonly canRedo: IObservable; + public get requestDisablement() { + return this._timeline.requestDisablement; + } + private readonly _onDidDispose = new Emitter(); get onDidDispose() { this._assertNotDisposed(); @@ -183,7 +187,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio @IBulkEditService public readonly _bulkEditService: IBulkEditService, @IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IChatService private readonly _chatService: IChatService, @INotebookService private readonly _notebookService: INotebookService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @ILogService private readonly _logService: ILogService, @@ -200,11 +203,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this.canUndo = this._timeline.canUndo.map((hasHistory, reader) => hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle); - this._register(autorun(reader => { - const disabled = this._timeline.requestDisablement.read(reader); - this._chatService.getSession(this.chatSessionResource)?.setDisabledRequests(disabled); - })); - this._init(transferFrom); } @@ -447,9 +445,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio override dispose() { this._assertNotDisposed(); - - this._chatService.cancelCurrentRequestForSession(this.chatSessionResource); - dispose(this._entriesObs.get()); super.dispose(); this._state.set(ChatEditingSessionState.Disposed, undefined); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index e6fb17e7706..28e18247be4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -26,6 +26,7 @@ import { IEditorGroup } from '../../../services/editor/common/editorGroupsServic import { ChatContextKeys } from '../common/chatContextKeys.js'; import { IChatModel, IExportableChatData, ISerializableChatData } from '../common/chatModel.js'; import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js'; +import { IChatService } from '../common/chatService.js'; import { IChatSessionsService, localChatSessionType } from '../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { clearChatEditor } from './actions/chatClear.js'; @@ -65,6 +66,7 @@ export class ChatEditor extends EditorPane { @IStorageService private readonly storageService: IStorageService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, ) { super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); } @@ -232,8 +234,8 @@ export class ChatEditor extends EditorPane { const viewState = options?.viewState ?? input.options.viewState; this.updateModel(editorModel.model, viewState); - if (isContributedChatSession && options?.title?.preferred) { - editorModel.model.setCustomTitle(options.title.preferred); + if (isContributedChatSession && options?.title?.preferred && input.sessionResource) { + this.chatService.setChatSessionTitle(input.sessionResource, options.title.preferred); } } catch (error) { this.hideLoadingInChatWidget(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 947444a93b3..78a0958c4fd 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -1784,7 +1784,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (isRequestVM(currentElement) && !this.viewModel?.editing) { const requests = this.viewModel?.model.getRequests(); - if (!requests) { + if (!requests || !this.viewModel?.sessionResource) { return; } diff --git a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts index 782b13d5eab..56440490660 100644 --- a/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/languageModelToolsService.ts @@ -306,7 +306,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo IChatToolInvocation.confirmWith(toolInvocation, autoConfirmed); } - model.acceptResponseProgress(request, toolInvocation); + this._chatService.appendProgress(request, toolInvocation); dto.toolSpecificData = toolInvocation?.toolSpecificData; if (preparedInvocation?.confirmationMessages?.title) { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 27a441b6e4d..3fc10ae285a 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -20,7 +20,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { IEditorPane } from '../../../common/editor.js'; import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { IChatAgentResult } from './chatAgents.js'; -import { ChatModel, IChatResponseModel } from './chatModel.js'; +import { ChatModel, IChatRequestDisablement, IChatResponseModel } from './chatModel.js'; import { IChatProgress } from './chatService.js'; export const IChatEditingService = createDecorator('chatEditingService'); @@ -117,6 +117,9 @@ export interface IChatEditingSession extends IDisposable { readonly onDidDispose: Event; readonly state: IObservable; readonly entries: IObservable; + /** Requests disabled by undo/redo in the session */ + readonly requestDisablement: IObservable; + show(previousChanges?: boolean): Promise; accept(...uris: URI[]): Promise; reject(...uris: URI[]): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index d8e0ca5870b..f8c173c2075 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -13,7 +13,7 @@ import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; -import { IObservable, autorunSelfDisposable, observableFromEvent, observableSignalFromEvent } from '../../../../base/common/observable.js'; +import { IObservable, autorun, autorunSelfDisposable, observableFromEvent, observableSignalFromEvent } 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'; @@ -216,9 +216,10 @@ export interface IChatResponseModel { export type ChatResponseModelChangeReason = | { reason: 'other' } + | { reason: 'completedRequest' } | { reason: 'undoStop'; id: string }; -const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; +export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; export interface IChatRequestModeInfo { kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' @@ -1011,13 +1012,13 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } this._isComplete = true; - this._onDidChange.fire(defaultChatResponseModelChangeReason); + this._onDidChange.fire({ reason: 'completedRequest' }); } cancel(): void { this._isComplete = true; this._isCanceled = true; - this._onDidChange.fire(defaultChatResponseModelChangeReason); + this._onDidChange.fire({ reason: 'completedRequest' }); } setFollowups(followups: IChatFollowup[] | undefined): void { @@ -1082,24 +1083,13 @@ export interface IChatModel extends IDisposable { readonly requestNeedsInput: IObservable; readonly inputPlaceholder?: string; readonly editingSession?: IChatEditingSession | undefined; - /** - * Sets requests as 'disabled', removing them from the UI. If a request ID - * is given without undo stops, it's removed entirely. If an undo stop - * is given, all content after that stop is removed. - */ - setDisabledRequests(requestIds: IChatRequestDisablement[]): void; + readonly checkpoint: IChatRequestModel | undefined; getRequests(): IChatRequestModel[]; setCheckpoint(requestId: string | undefined): void; - readonly checkpoint: IChatRequestModel | undefined; - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools): IChatRequestModel; - acceptResponseProgress(request: IChatRequestModel, progress: IChatProgress, quiet?: boolean): void; - setResponse(request: IChatRequestModel, result: IChatAgentResult): void; - completeResponse(request: IChatRequestModel): void; - setCustomTitle(title: string): void; + toExport(): IExportableChatData; toJSON(): ISerializableChatData; readonly contributedChatSession: IChatSessionContext | undefined; - setContributedChatSession(session: IChatSessionContext | undefined): void; } export interface ISerializableChatsData { @@ -1320,7 +1310,6 @@ export interface IChatRemoveRequestEvent { export interface IChatSetHiddenEvent { kind: 'setHidden'; - hiddenRequestIds: readonly IChatRequestDisablement[]; } export interface IChatMoveEvent { @@ -1482,25 +1471,42 @@ export class ChatModel extends Disposable implements IChatModel { this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation; this._canUseTools = initialModelProps.canUseTools; - const lastResponse = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1)?.response); + const lastRequest = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1)); - this.requestInProgress = lastResponse.map((response, r) => { - return response?.isInProgress.read(r) ?? false; + this._register(autorun(reader => { + const request = lastRequest.read(reader); + if (!request?.response) { + return; + } + + reader.store.add(request.response.onDidChange(ev => { + if (ev.reason === 'completedRequest') { + this._onDidChange.fire({ kind: 'completedRequest', request }); + } + })); + })); + + this.requestInProgress = lastRequest.map((request, r) => { + return request?.response?.isInProgress.read(r) ?? false; }); - this.requestNeedsInput = lastResponse.map((response, r) => { - return response?.isPendingConfirmation.read(r) ?? false; + this.requestNeedsInput = lastRequest.map((request, r) => { + return request?.response?.isPendingConfirmation.read(r) ?? false; }); } startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void { - this._editingSession ??= this._register( + const session = this._editingSession ??= this._register( transferFromSession ? this.chatEditingService.transferEditingSession(this, transferFromSession) : isGlobalEditingSession ? this.chatEditingService.startOrContinueGlobalEditingSession(this) : this.chatEditingService.createEditingSession(this) ); + + this._register(autorun(reader => { + this._setDisabledRequests(session.requestDisablement.read(reader)); + })); } private currentEditedFileEvents = new ResourceMap(); @@ -1678,7 +1684,7 @@ export class ChatModel extends Disposable implements IChatModel { return this._checkpoint; } - setDisabledRequests(requestIds: IChatRequestDisablement[]) { + private _setDisabledRequests(requestIds: IChatRequestDisablement[]) { this._requests.forEach((request) => { const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id); request.shouldBeRemovedOnSend = shouldBeRemovedOnSend; @@ -1687,10 +1693,7 @@ export class ChatModel extends Disposable implements IChatModel { } }); - this._onDidChange.fire({ - kind: 'setHidden', - hiddenRequestIds: requestIds, - }); + this._onDidChange.fire({ kind: 'setHidden' }); } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools): ChatRequestModel { @@ -1770,7 +1773,6 @@ export class ChatModel extends Disposable implements IChatModel { throw new Error('acceptResponseProgress: Adding progress to a completed response'); } - if (progress.kind === 'usedContext' || progress.kind === 'reference') { request.response.applyReference(progress); } else if (progress.kind === 'codeCitation') { @@ -1818,15 +1820,6 @@ export class ChatModel extends Disposable implements IChatModel { request.response.setResult(result); } - completeResponse(request: ChatRequestModel): void { - if (!request.response) { - throw new Error('Call setResponse before completeResponse'); - } - - request.response.complete(); - this._onDidChange.fire({ kind: 'completedRequest', request }); - } - setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void { if (!request.response) { // Maybe something went wrong? diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index a567027daca..fb33b171655 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -959,6 +959,11 @@ export interface IChatService { */ sendRequest(sessionResource: URI, message: string, options?: IChatSendRequestOptions): Promise; + /** + * Sets a custom title for a chat model. + */ + setTitle(sessionResource: URI, title: string): void; + appendProgress(request: IChatRequestModel, progress: IChatProgress): void; resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; removeRequest(sessionResource: URI, requestId: string): Promise; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 9adc2c0bb13..1d754ec12c8 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -6,7 +6,7 @@ import { DeferredPromise } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; +import { BugIndicatingError, ErrorNoTelemetry } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; @@ -592,7 +592,7 @@ export class ChatService extends Disposable implements IChatService { for (const message of providedSession.history) { if (message.type === 'request') { if (lastRequest) { - model.completeResponse(lastRequest); + lastRequest.response?.complete(); } const requestText = message.prompt; @@ -666,13 +666,13 @@ export class ChatService extends Disposable implements IChatService { // Handle completion if (isComplete) { - model?.completeResponse(lastRequest); + lastRequest.response?.complete(); cancellationListener.clear(); } })); } else { if (lastRequest) { - model.completeResponse(lastRequest); + lastRequest.response?.complete(); } } @@ -1051,7 +1051,7 @@ export class ChatService extends Disposable implements IChatService { completeResponseCreated(); this.trace('sendRequest', `Provider returned response for session ${model.sessionResource}`); - model.completeResponse(request); + request.response?.complete(); if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request, followups); @@ -1079,7 +1079,7 @@ export class ChatService extends Disposable implements IChatService { const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; model.setResponse(request, rawResult); completeResponseCreated(); - model.completeResponse(request); + request.response?.complete(); } } finally { store.dispose(); @@ -1216,7 +1216,7 @@ export class ChatService extends Disposable implements IChatService { if (response.followups !== undefined) { model.setFollowups(request, response.followups); } - model.completeResponse(request); + request.response?.complete(); } cancelCurrentRequestForSession(sessionResource: URI): void { @@ -1282,6 +1282,19 @@ export class ChatService extends Disposable implements IChatService { this._chatSessionStore.logIndex(); } + setTitle(sessionResource: URI, title: string): void { + this._sessionModels.get(sessionResource)?.setCustomTitle(title); + } + + appendProgress(request: IChatRequestModel, progress: IChatProgress): void { + const model = this._sessionModels.get(request.session.sessionResource); + if (!(request instanceof ChatRequestModel)) { + throw new BugIndicatingError('Can only append progress to requests of type ChatRequestModel'); + } + + model?.acceptResponseProgress(request, progress); + } + private toLocalSessionId(sessionResource: URI) { const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); if (!localSessionId) { diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 3caf2cc774e..3ea6973556c 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -21,7 +21,7 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js'; -import { IChatModel } from '../../common/chatModel.js'; +import { ChatModel, IChatModel } from '../../common/chatModel.js'; import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService.js'; import { ChatConfiguration } from '../../common/constants.js'; import { GithubCopilotToolReference, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/languageModelToolsService.js'; @@ -89,9 +89,12 @@ function stubGetSession(chatService: MockChatService, sessionId: string, options sessionId, sessionResource: LocalChatSessionUri.forSession(sessionId), getRequests: () => [{ id: requestId, modelId: 'test-model' }], - acceptResponseProgress: (_req: any, progress: any) => { if (capture) { capture.invocation = progress; } }, - } as IChatModel; + } as ChatModel; chatService.addSession(fakeModel); + chatService.appendProgress = (request, progress) => { + if (capture) { capture.invocation = progress; } + }; + return fakeModel; } diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts index 558e57c2472..8ae45ccdafa 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -147,7 +147,10 @@ suite('ChatService', () => { instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None }); instantiationService.stub(IChatEditingService, new class extends mock() { override startOrContinueGlobalEditingSession(): IChatEditingSession { - return Disposable.None as IChatEditingSession; + return { + requestDisablement: observableValue('requestDisablement', []), + dispose: () => { } + } as unknown as IChatEditingSession; } }); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 507e8500cae..cbcd2c0df74 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -10,7 +10,7 @@ import { observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js'; import { IParsedChatRequest } from '../../common/chatParserTypes.js'; -import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js'; +import { IChatCompleteResponse, IChatDetail, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js'; import { ChatAgentLocation } from '../../common/constants.js'; export class MockChatService implements IChatService { @@ -53,6 +53,12 @@ export class MockChatService implements IChatService { } loadSessionForResource(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { throw new Error('Method not implemented.'); + } + setTitle(sessionResource: URI, title: string): void { + throw new Error('Method not implemented.'); + } + appendProgress(request: IChatRequestModel, progress: IChatProgress): void { + } /** * Returns whether the request was accepted. diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 51efc31b32f..698b89d9207 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -1616,7 +1616,7 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); if (!token.isCancellationRequested) { - chatModel.completeResponse(chatRequest); + chatRequest.response.complete(); } const isSettled = derived(r => {