mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-26 11:38:51 +01:00
remove bridge agents and agent providers because inline chat is all participants now
* remove much of IInlineChatSessionProvider * deprecate IInlineChatService and move most consumers off
This commit is contained in:
@@ -2,216 +2,36 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { coalesceInPlace, isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { CancellationError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { DisposableMap, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { LRUCache } from 'vs/base/common/map';
|
||||
import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { TextEdit, WorkspaceEdit } from 'vs/editor/common/languages';
|
||||
import { ITextModel, IValidEditOperation } from 'vs/editor/common/model';
|
||||
import { IValidEditOperation } from 'vs/editor/common/model';
|
||||
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
|
||||
import { IModelService } from 'vs/editor/common/services/model';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgress, Progress } from 'vs/platform/progress/common/progress';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor';
|
||||
import { ChatAgentLocation, IChatAgent, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentResult, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
|
||||
import { IChatFollowup, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
|
||||
import { EditMode, IInlineChatBulkEditResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatResponse, IInlineChatService, IInlineChatSession, IInlineChatSessionProvider, IInlineChatSlashCommand, InlineChatResponseFeedbackKind, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
|
||||
import { ChatAgentLocation, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
|
||||
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
|
||||
import { CTX_INLINE_CHAT_HAS_PROVIDER, EditMode, IInlineChatBulkEditResponse, IInlineChatSession, IInlineChatSlashCommand, InlineChatResponseType } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput';
|
||||
import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession';
|
||||
import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer, Recording } from './inlineChatSessionService';
|
||||
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
|
||||
import { ISelection } from 'vs/editor/common/core/selection';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
class BridgeAgent implements IChatAgentImplementation {
|
||||
|
||||
constructor(
|
||||
private readonly _data: IChatAgentData,
|
||||
private readonly _sessions: ReadonlyMap<string, SessionData>,
|
||||
private readonly _postLastResponse: (data: { id: string; response: ReplyResponse | ErrorResponse | EmptyResponse }) => void,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
) { }
|
||||
|
||||
|
||||
private _findSessionDataByRequest(request: IChatAgentRequest) {
|
||||
let data: SessionData | undefined;
|
||||
for (const candidate of this._sessions.values()) {
|
||||
if (candidate.session.chatModel.sessionId === request.sessionId) {
|
||||
data = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async invoke(request: IChatAgentRequest, progress: (part: IChatProgress) => void, _history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> {
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const data = this._findSessionDataByRequest(request);
|
||||
|
||||
if (!data) {
|
||||
throw new Error('FAILED to find session');
|
||||
}
|
||||
|
||||
const { session } = data;
|
||||
|
||||
if (!session.lastInput) {
|
||||
throw new Error('FAILED to find last input');
|
||||
}
|
||||
|
||||
const inlineChatContextValue = request.variables.variables.find(candidate => candidate.name === _inlineChatContext)?.value;
|
||||
const inlineChatContext = typeof inlineChatContextValue === 'string' && JSON.parse(inlineChatContextValue);
|
||||
|
||||
const modelAltVersionIdNow = session.textModelN.getAlternativeVersionId();
|
||||
const progressEdits: TextEdit[][] = [];
|
||||
|
||||
const inlineRequest: IInlineChatRequest = {
|
||||
requestId: request.requestId,
|
||||
prompt: request.message,
|
||||
attempt: request.attempt ?? 0,
|
||||
withIntentDetection: request.enableCommandDetection ?? true,
|
||||
live: session.editMode !== EditMode.Preview,
|
||||
previewDocument: session.textModelN.uri,
|
||||
selection: inlineChatContext.selection,
|
||||
wholeRange: inlineChatContext.wholeRange
|
||||
};
|
||||
|
||||
const inlineProgress = new Progress<IInlineChatProgressItem>(data => {
|
||||
// TODO@jrieken
|
||||
// if (data.message) {
|
||||
// progress({ kind: 'progressMessage', content: new MarkdownString(data.message) });
|
||||
// }
|
||||
// TODO@ulugbekna,jrieken should we only send data.slashCommand when having detected one?
|
||||
if (data.slashCommand && !inlineRequest.prompt.startsWith('/')) {
|
||||
const command = this._data.slashCommands.find(c => c.name === data.slashCommand);
|
||||
progress({ kind: 'agentDetection', agentId: this._data.id, command });
|
||||
}
|
||||
if (data.markdownFragment) {
|
||||
progress({ kind: 'markdownContent', content: new MarkdownString(data.markdownFragment) });
|
||||
}
|
||||
if (isNonEmptyArray(data.edits)) {
|
||||
progressEdits.push(data.edits);
|
||||
progress({ kind: 'textEdit', uri: session.textModelN.uri, edits: data.edits });
|
||||
}
|
||||
});
|
||||
|
||||
let result: IInlineChatResponse | undefined | null;
|
||||
let response: ReplyResponse | ErrorResponse | EmptyResponse;
|
||||
|
||||
try {
|
||||
result = await data.session.provider.provideResponse(session.session, inlineRequest, inlineProgress, token);
|
||||
|
||||
if (result) {
|
||||
if (result.message) {
|
||||
inlineProgress.report({ markdownFragment: result.message.value });
|
||||
}
|
||||
if (Array.isArray(result.edits)) {
|
||||
inlineProgress.report({ edits: result.edits });
|
||||
}
|
||||
|
||||
const markdownContents = result.message ?? new MarkdownString('', { supportThemeIcons: true, supportHtml: true, isTrusted: false });
|
||||
|
||||
const chatModelRequest = session.chatModel.getRequests().find(candidate => candidate.id === request.requestId);
|
||||
|
||||
response = this._instaService.createInstance(ReplyResponse, result, markdownContents, session.textModelN.uri, modelAltVersionIdNow, progressEdits, request.requestId, chatModelRequest?.response);
|
||||
|
||||
} else {
|
||||
response = new EmptyResponse();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
response = new ErrorResponse(e);
|
||||
}
|
||||
|
||||
this._postLastResponse({ id: request.requestId, response });
|
||||
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
inlineChatResponse: result
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async provideFollowups(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> {
|
||||
|
||||
if (!result.metadata?.inlineChatResponse) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = this._findSessionDataByRequest(request);
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inlineFollowups = await data.session.provider.provideFollowups?.(data.session.session, result.metadata?.inlineChatResponse, token);
|
||||
if (!inlineFollowups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const chatFollowups = inlineFollowups.map(f => {
|
||||
if (f.kind === 'reply') {
|
||||
return {
|
||||
kind: 'reply',
|
||||
message: f.message,
|
||||
agentId: request.agentId,
|
||||
title: f.title,
|
||||
tooltip: f.tooltip,
|
||||
} satisfies IChatFollowup;
|
||||
} else {
|
||||
// TODO@jrieken update API
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
coalesceInPlace(chatFollowups);
|
||||
return chatFollowups;
|
||||
}
|
||||
|
||||
provideWelcomeMessage(location: ChatAgentLocation, token: CancellationToken): string[] {
|
||||
// without this provideSampleQuestions is not called
|
||||
return [];
|
||||
}
|
||||
|
||||
async provideSampleQuestions(location: ChatAgentLocation, token: CancellationToken): Promise<IChatFollowup[]> {
|
||||
// TODO@jrieken DEBT
|
||||
// (hack) this function is called while creating the session. We need the timeout to make sure this._sessions is populated.
|
||||
// (hack) we have no context/session id and therefore use the first session with an active editor
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
for (const [, data] of this._sessions) {
|
||||
if (data.session.session.input && data.editor.hasWidgetFocus()) {
|
||||
return [{
|
||||
kind: 'reply',
|
||||
agentId: _bridgeAgentId,
|
||||
message: data.session.session.input,
|
||||
}];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
type SessionData = {
|
||||
editor: ICodeEditor;
|
||||
@@ -227,7 +47,6 @@ export class InlineChatError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const _bridgeAgentId = 'brigde.editor';
|
||||
const _inlineChatContext = '_inlineChatContext';
|
||||
const _inlineChatDocument = '_inlineChatDocument';
|
||||
|
||||
@@ -264,10 +83,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
private readonly _keyComputers = new Map<string, ISessionKeyComputer>();
|
||||
private _recordings: Recording[] = [];
|
||||
|
||||
private readonly _lastResponsesFromBridgeAgent = new LRUCache<string, ReplyResponse | EmptyResponse | ErrorResponse>(5);
|
||||
|
||||
constructor(
|
||||
@IInlineChatService private readonly _inlineChatService: IInlineChatService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@@ -280,103 +97,6 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
@IChatVariablesService chatVariableService: IChatVariablesService,
|
||||
) {
|
||||
|
||||
const fakeProviders = this._store.add(new DisposableMap<string, IDisposable>());
|
||||
|
||||
this._store.add(this._chatAgentService.onDidChangeAgents(() => {
|
||||
|
||||
const providersNow = new Set<string>();
|
||||
|
||||
for (const agent of this._chatAgentService.getActivatedAgents()) {
|
||||
if (agent.id === _bridgeAgentId) {
|
||||
// not interesting
|
||||
continue;
|
||||
}
|
||||
if (!agent.locations.includes(ChatAgentLocation.Editor) || !agent.isDefault) {
|
||||
// not interesting
|
||||
continue;
|
||||
}
|
||||
providersNow.add(agent.id);
|
||||
|
||||
if (!fakeProviders.has(agent.id)) {
|
||||
fakeProviders.set(agent.id, _inlineChatService.addProvider(_instaService.createInstance(AgentInlineChatProvider, agent)));
|
||||
this._logService.debug(`ADDED inline chat provider for agent ${agent.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id] of fakeProviders) {
|
||||
if (!providersNow.has(id)) {
|
||||
fakeProviders.deleteAndDispose(id);
|
||||
this._logService.debug(`REMOVED inline chat provider for agent ${id}`);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// MARK: register fake chat agent
|
||||
const addOrRemoveBridgeAgent = () => {
|
||||
const that = this;
|
||||
const agentData: IChatAgentData = {
|
||||
id: _bridgeAgentId,
|
||||
name: 'editor',
|
||||
extensionId: nullExtensionDescription.identifier,
|
||||
publisherDisplayName: '',
|
||||
extensionDisplayName: '',
|
||||
extensionPublisherId: '',
|
||||
isDefault: true,
|
||||
locations: [ChatAgentLocation.Editor],
|
||||
get slashCommands(): IChatAgentCommand[] {
|
||||
// HACK@jrieken
|
||||
// find the active session and return its slash commands
|
||||
let candidate: Session | undefined;
|
||||
for (const data of that._sessions.values()) {
|
||||
if (data.editor.hasWidgetFocus()) {
|
||||
candidate = data.session;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!candidate || !candidate.session.slashCommands) {
|
||||
return [];
|
||||
}
|
||||
return candidate.session.slashCommands.map(c => {
|
||||
return {
|
||||
name: c.command,
|
||||
description: c.detail ?? '',
|
||||
} satisfies IChatAgentCommand;
|
||||
});
|
||||
},
|
||||
defaultImplicitVariables: [_inlineChatContext],
|
||||
metadata: {
|
||||
isSticky: false,
|
||||
themeIcon: Codicon.copilot,
|
||||
},
|
||||
};
|
||||
|
||||
let otherEditorAgent: IChatAgentData | undefined;
|
||||
let myEditorAgent: IChatAgentData | undefined;
|
||||
|
||||
for (const candidate of this._chatAgentService.getActivatedAgents()) {
|
||||
if (!myEditorAgent && candidate.id === agentData.id) {
|
||||
myEditorAgent = candidate;
|
||||
} else if (!otherEditorAgent && candidate.isDefault && candidate.locations.includes(ChatAgentLocation.Editor)) {
|
||||
otherEditorAgent = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (otherEditorAgent) {
|
||||
bridgeStore.clear();
|
||||
_logService.debug(`REMOVED bridge agent "${agentData.id}", found "${otherEditorAgent.id}"`);
|
||||
|
||||
} else if (!myEditorAgent) {
|
||||
bridgeStore.value = this._chatAgentService.registerDynamicAgent(agentData, this._instaService.createInstance(BridgeAgent, agentData, this._sessions, data => {
|
||||
this._lastResponsesFromBridgeAgent.set(data.id, data.response);
|
||||
}));
|
||||
_logService.debug(`ADDED bridge agent "${agentData.id}"`);
|
||||
}
|
||||
};
|
||||
|
||||
this._store.add(this._chatAgentService.onDidChangeAgents(() => addOrRemoveBridgeAgent()));
|
||||
const bridgeStore = this._store.add(new MutableDisposable());
|
||||
addOrRemoveBridgeAgent();
|
||||
|
||||
|
||||
// MARK: implicit variable for editor selection and (tracked) whole range
|
||||
|
||||
@@ -414,47 +134,34 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: Range }, token: CancellationToken): Promise<Session | undefined> {
|
||||
|
||||
const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor);
|
||||
let provider: IInlineChatSessionProvider | undefined;
|
||||
if (agent) {
|
||||
for (const candidate of this._inlineChatService.getAllProvider()) {
|
||||
if (candidate instanceof AgentInlineChatProvider && candidate.agent === agent) {
|
||||
provider = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
provider = Iterable.first(this._inlineChatService.getAllProvider());
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
this._logService.trace('[IE] NO provider found');
|
||||
if (!agent) {
|
||||
this._logService.trace('[IE] NO agent found');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
this._onWillStartSession.fire(editor);
|
||||
|
||||
const textModel = editor.getModel();
|
||||
const selection = editor.getSelection();
|
||||
let rawSession: IInlineChatSession | undefined | null;
|
||||
try {
|
||||
rawSession = await raceCancellation(
|
||||
Promise.resolve(provider.prepareInlineChatSession(textModel, selection, token)),
|
||||
token
|
||||
);
|
||||
} catch (error) {
|
||||
this._logService.error('[IE] FAILED to prepare session', provider.extensionId);
|
||||
this._logService.error(error);
|
||||
throw new InlineChatError((error as Error)?.message || 'Failed to prepare session');
|
||||
}
|
||||
if (!rawSession) {
|
||||
this._logService.trace('[IE] NO session', provider.extensionId);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rawSession: IInlineChatSession = {
|
||||
id: Math.random(),
|
||||
wholeRange: new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn),
|
||||
placeholder: agent.description,
|
||||
slashCommands: agent.slashCommands.map(agentCommand => {
|
||||
return {
|
||||
command: agentCommand.name,
|
||||
detail: agentCommand.description,
|
||||
refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up
|
||||
} satisfies IInlineChatSlashCommand;
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
const store = new DisposableStore();
|
||||
this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${provider.extensionId}`);
|
||||
this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`);
|
||||
|
||||
const chatModel = this._chatService.startSession(ChatAgentLocation.Editor, token);
|
||||
if (!chatModel) {
|
||||
@@ -486,59 +193,52 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
lastResponseListener.clear(); // ONCE
|
||||
|
||||
let inlineResponse: ErrorResponse | EmptyResponse | ReplyResponse;
|
||||
if (response.agent?.id === _bridgeAgentId) {
|
||||
// use result that was provided by
|
||||
inlineResponse = this._lastResponsesFromBridgeAgent.get(response.requestId) ?? new ErrorResponse(new Error('Missing Response'));
|
||||
this._lastResponsesFromBridgeAgent.delete(response.requestId);
|
||||
|
||||
// make an response from the ChatResponseModel
|
||||
if (response.isCanceled) {
|
||||
// error: cancelled
|
||||
inlineResponse = new ErrorResponse(new CancellationError());
|
||||
} else if (response.result?.errorDetails) {
|
||||
// error: "real" error
|
||||
inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message));
|
||||
} else if (response.response.value.length === 0) {
|
||||
// epmty response
|
||||
inlineResponse = new EmptyResponse();
|
||||
} else {
|
||||
// make an artificial response from the ChatResponseModel
|
||||
if (response.isCanceled) {
|
||||
// error: cancelled
|
||||
inlineResponse = new ErrorResponse(new CancellationError());
|
||||
} else if (response.result?.errorDetails) {
|
||||
// error: "real" error
|
||||
inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message));
|
||||
} else if (response.response.value.length === 0) {
|
||||
// epmty response
|
||||
inlineResponse = new EmptyResponse();
|
||||
} else {
|
||||
// replay response
|
||||
const markdownContent = new MarkdownString();
|
||||
const raw: IInlineChatBulkEditResponse = {
|
||||
id: Math.random(),
|
||||
type: InlineChatResponseType.BulkEdit,
|
||||
message: markdownContent,
|
||||
edits: { edits: [] },
|
||||
};
|
||||
for (const item of response.response.value) {
|
||||
if (item.kind === 'markdownContent') {
|
||||
markdownContent.value += item.content.value;
|
||||
} else if (item.kind === 'textEditGroup') {
|
||||
for (const group of item.edits) {
|
||||
for (const edit of group) {
|
||||
raw.edits.edits.push({
|
||||
resource: item.uri,
|
||||
textEdit: edit,
|
||||
versionId: undefined
|
||||
});
|
||||
}
|
||||
// replay response
|
||||
const markdownContent = new MarkdownString();
|
||||
const raw: IInlineChatBulkEditResponse = {
|
||||
id: Math.random(),
|
||||
type: InlineChatResponseType.BulkEdit,
|
||||
message: markdownContent,
|
||||
edits: { edits: [] },
|
||||
};
|
||||
for (const item of response.response.value) {
|
||||
if (item.kind === 'markdownContent') {
|
||||
markdownContent.value += item.content.value;
|
||||
} else if (item.kind === 'textEditGroup') {
|
||||
for (const group of item.edits) {
|
||||
for (const edit of group) {
|
||||
raw.edits.edits.push({
|
||||
resource: item.uri,
|
||||
textEdit: edit,
|
||||
versionId: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inlineResponse = this._instaService.createInstance(
|
||||
ReplyResponse,
|
||||
raw,
|
||||
markdownContent,
|
||||
session.textModelN.uri,
|
||||
modelAltVersionIdNow,
|
||||
[],
|
||||
e.request.id,
|
||||
e.request.response
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
inlineResponse = this._instaService.createInstance(
|
||||
ReplyResponse,
|
||||
raw,
|
||||
markdownContent,
|
||||
session.textModelN.uri,
|
||||
modelAltVersionIdNow,
|
||||
[],
|
||||
e.request.id,
|
||||
e.request.response
|
||||
);
|
||||
}
|
||||
|
||||
session.addExchange(new SessionExchange(session.lastInput!, inlineResponse));
|
||||
@@ -551,38 +251,9 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
});
|
||||
}));
|
||||
|
||||
store.add(this._chatService.onDidPerformUserAction(e => {
|
||||
if (e.sessionId !== chatModel.sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO@jrieken VALIDATE candidate is proper, e.g check with `session.exchanges`
|
||||
const request = chatModel.getRequests().find(request => request.id === e.requestId);
|
||||
const candidate = request?.response?.result?.metadata?.inlineChatResponse;
|
||||
|
||||
if (!candidate) {
|
||||
return;
|
||||
}
|
||||
|
||||
let kind: InlineChatResponseFeedbackKind | undefined;
|
||||
if (e.action.kind === 'vote') {
|
||||
kind = e.action.direction === ChatAgentVoteDirection.Down ? InlineChatResponseFeedbackKind.Unhelpful : InlineChatResponseFeedbackKind.Helpful;
|
||||
} else if (e.action.kind === 'bug') {
|
||||
kind = InlineChatResponseFeedbackKind.Bug;
|
||||
} else if (e.action.kind === 'inlineChat') {
|
||||
kind = e.action.action === 'accepted' ? InlineChatResponseFeedbackKind.Accepted : InlineChatResponseFeedbackKind.Undone;
|
||||
}
|
||||
|
||||
if (!kind) {
|
||||
return;
|
||||
}
|
||||
|
||||
provider.handleInlineChatResponseFeedback?.(rawSession, candidate, kind);
|
||||
}));
|
||||
|
||||
store.add(this._inlineChatService.onDidChangeProviders(e => {
|
||||
if (e.removed === provider) {
|
||||
this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${provider.extensionId}`);
|
||||
store.add(this._chatAgentService.onDidChangeAgents(e => {
|
||||
if (e === undefined && !this._chatAgentService.getAgent(agent.id)) {
|
||||
this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`);
|
||||
this._releaseSession(session, true);
|
||||
}
|
||||
}));
|
||||
@@ -625,7 +296,8 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
targetUri,
|
||||
textModel0,
|
||||
textModelN,
|
||||
provider, rawSession,
|
||||
agent,
|
||||
rawSession,
|
||||
store.add(new SessionWholeRange(textModelN, wholeRange)),
|
||||
store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)),
|
||||
chatModel
|
||||
@@ -659,7 +331,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
found = true;
|
||||
this._sessions.delete(oldKey);
|
||||
this._sessions.set(newKey, { ...data, editor: target });
|
||||
this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.provider.extensionId}`);
|
||||
this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`);
|
||||
this._onDidMoveSession.fire({ session, editor: target });
|
||||
break;
|
||||
}
|
||||
@@ -696,7 +368,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
|
||||
const [key, value] = tuple;
|
||||
this._sessions.delete(key);
|
||||
this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.provider.extensionId}`);
|
||||
this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`);
|
||||
|
||||
this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer });
|
||||
value.store.dispose();
|
||||
@@ -706,7 +378,7 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
this._keepRecording(session);
|
||||
const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits);
|
||||
this._onDidStashSession.fire({ editor, session });
|
||||
this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.provider.extensionId}`);
|
||||
this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -751,89 +423,24 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
|
||||
}
|
||||
}
|
||||
|
||||
export class AgentInlineChatProvider implements IInlineChatSessionProvider {
|
||||
export class InlineChatEnabler {
|
||||
|
||||
readonly extensionId: ExtensionIdentifier;
|
||||
readonly label: string;
|
||||
readonly supportIssueReporting?: boolean | undefined;
|
||||
static Id = 'inlineChat.enabler';
|
||||
|
||||
private readonly _ctxHasProvider: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
readonly agent: IChatAgent,
|
||||
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IChatAgentService chatAgentService: IChatAgentService
|
||||
) {
|
||||
this.label = agent.fullName ?? agent.name;
|
||||
this.extensionId = agent.extensionId;
|
||||
this.supportIssueReporting = agent.metadata.supportIssueReporting;
|
||||
this._ctxHasProvider = CTX_INLINE_CHAT_HAS_PROVIDER.bindTo(contextKeyService);
|
||||
chatAgentService.onDidChangeAgents(() => {
|
||||
const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(ChatAgentLocation.Editor));
|
||||
this._ctxHasProvider.set(hasEditorAgent);
|
||||
});
|
||||
}
|
||||
|
||||
async prepareInlineChatSession(model: ITextModel, range: ISelection, token: CancellationToken): Promise<IInlineChatSession> {
|
||||
|
||||
// TODO@jrieken have a good welcome message
|
||||
// const welcomeMessage = await this.agent.provideWelcomeMessage?.(ChatAgentLocation.Editor, token);
|
||||
// const message = welcomeMessage?.filter(candidate => typeof candidate === 'string').join(''),
|
||||
|
||||
return {
|
||||
id: Math.random(),
|
||||
wholeRange: new Range(range.selectionStartLineNumber, range.selectionStartColumn, range.positionLineNumber, range.positionColumn),
|
||||
placeholder: this.agent.description,
|
||||
slashCommands: this.agent.slashCommands.map(agentCommand => {
|
||||
return {
|
||||
command: agentCommand.name,
|
||||
detail: agentCommand.description,
|
||||
refer: agentCommand.name === 'explain' // TODO@jrieken @joyceerhl this should be cleaned up
|
||||
} satisfies IInlineChatSlashCommand;
|
||||
})
|
||||
};
|
||||
dispose() {
|
||||
this._ctxHasProvider.reset();
|
||||
}
|
||||
|
||||
async provideResponse(item: IInlineChatSession, request: IInlineChatRequest, progress: IProgress<IInlineChatProgressItem>, token: CancellationToken): Promise<IInlineChatResponse> {
|
||||
|
||||
const workspaceEdit: WorkspaceEdit = { edits: [] };
|
||||
|
||||
await this._chatAgentService.invokeAgent(this.agent.id, {
|
||||
sessionId: String(item.id),
|
||||
requestId: request.requestId,
|
||||
agentId: this.agent.id,
|
||||
message: request.prompt,
|
||||
location: ChatAgentLocation.Editor,
|
||||
variables: {
|
||||
variables: [{
|
||||
id: InlineChatContext.variableName,
|
||||
name: InlineChatContext.variableName,
|
||||
value: JSON.stringify(new InlineChatContext(request.previewDocument, request.selection, request.wholeRange))
|
||||
}]
|
||||
}
|
||||
}, part => {
|
||||
|
||||
if (part.kind === 'markdownContent') {
|
||||
progress.report({ markdownFragment: part.content.value });
|
||||
} else if (part.kind === 'agentDetection') {
|
||||
progress.report({ slashCommand: part.command?.name });
|
||||
} else if (part.kind === 'textEdit') {
|
||||
|
||||
if (isEqual(request.previewDocument, part.uri)) {
|
||||
progress.report({ edits: part.edits });
|
||||
} else {
|
||||
for (const textEdit of part.edits) {
|
||||
workspaceEdit.edits.push({ resource: part.uri, textEdit, versionId: undefined });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, [], token);
|
||||
|
||||
return {
|
||||
type: InlineChatResponseType.BulkEdit,
|
||||
id: Math.random(),
|
||||
edits: workspaceEdit
|
||||
};
|
||||
}
|
||||
|
||||
// handleInlineChatResponseFeedback?(session: IInlineChatSession, response: IInlineChatResponse, kind: InlineChatResponseFeedbackKind): void {
|
||||
// throw new Error('Method not implemented.');
|
||||
// }
|
||||
|
||||
// provideFollowups?(session: IInlineChatSession, response: IInlineChatResponse, token: CancellationToken): ProviderResult<IInlineChatFollowup[]> {
|
||||
// throw new Error('Method not implemented.');
|
||||
// }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user