adopt chat agent in terminal chat (#214499)

This commit is contained in:
Megan Rogge 2024-06-07 13:20:08 -07:00 committed by GitHub
parent 739a9e8579
commit 52c722c7bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 183 additions and 239 deletions

View file

@ -448,12 +448,12 @@ export class InlineChatWidget {
if (!viewModel) {
return undefined;
}
for (const item of viewModel.getItems()) {
if (isResponseVM(item)) {
return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model;
}
const items = viewModel.getItems().filter(i => isResponseVM(i));
if (!items.length) {
return;
}
return undefined;
const item = items[items.length - 1];
return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model;
}
get responseContent(): string | undefined {

View file

@ -15,9 +15,6 @@ export const enum TerminalChatCommandId {
Discard = 'workbench.action.terminal.chat.discard',
MakeRequest = 'workbench.action.terminal.chat.makeRequest',
Cancel = 'workbench.action.terminal.chat.cancel',
FeedbackHelpful = 'workbench.action.terminal.chat.feedbackHelpful',
FeedbackUnhelpful = 'workbench.action.terminal.chat.feedbackUnhelpful',
FeedbackReportIssue = 'workbench.action.terminal.chat.feedbackReportIssue',
RunCommand = 'workbench.action.terminal.chat.runCommand',
RunFirstCommand = 'workbench.action.terminal.chat.runFirstCommand',
InsertCommand = 'workbench.action.terminal.chat.insertCommand',
@ -30,7 +27,6 @@ export const enum TerminalChatCommandId {
export const MENU_TERMINAL_CHAT_INPUT = MenuId.for('terminalChatInput');
export const MENU_TERMINAL_CHAT_WIDGET = MenuId.for('terminalChatWidget');
export const MENU_TERMINAL_CHAT_WIDGET_STATUS = MenuId.for('terminalChatWidget.status');
export const MENU_TERMINAL_CHAT_WIDGET_FEEDBACK = MenuId.for('terminalChatWidget.feedback');
export const MENU_TERMINAL_CHAT_WIDGET_TOOLBAR = MenuId.for('terminalChatWidget.toolbar');
export const enum TerminalChatContextKeyStrings {
@ -61,18 +57,9 @@ export namespace TerminalChatContextKeys {
/** Whether the chat input has text */
export const inputHasText = new RawContextKey<boolean>(TerminalChatContextKeyStrings.ChatInputHasText, false, localize('chatInputHasTextContextKey', "Whether the chat input has text."));
/** Whether the terminal chat agent has been registered */
export const agentRegistered = new RawContextKey<boolean>(TerminalChatContextKeyStrings.ChatAgentRegistered, false, localize('chatAgentRegisteredContextKey', "Whether the terminal chat agent has been registered."));
/** The chat response contains at least one code block */
export const responseContainsCodeBlock = new RawContextKey<boolean>(TerminalChatContextKeyStrings.ChatResponseContainsCodeBlock, false, localize('chatResponseContainsCodeBlockContextKey', "Whether the chat response contains a code block."));
/** The chat response contains multiple code blocks */
export const responseContainsMultipleCodeBlocks = new RawContextKey<boolean>(TerminalChatContextKeyStrings.ChatResponseContainsMultipleCodeBlocks, false, localize('chatResponseContainsMultipleCodeBlocksContextKey', "Whether the chat response contains multiple code blocks."));
/** Whether the response supports issue reporting */
export const responseSupportsIssueReporting = new RawContextKey<boolean>(TerminalChatContextKeyStrings.ChatResponseSupportsIssueReporting, false, localize('chatResponseSupportsIssueReportingContextKey', "Whether the response supports issue reporting"));
/** The chat vote, if any for the response, if any */
export const sessionResponseVote = new RawContextKey<string>(TerminalChatContextKeyStrings.ChatSessionResponseVote, undefined, { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") });
}

View file

@ -13,7 +13,7 @@ import { CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_HAS_AGE
import { isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { registerActiveXtermAction } from 'vs/workbench/contrib/terminal/browser/terminalActions';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat';
import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat';
import { TerminalChatController } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController';
registerActiveXtermAction({
@ -164,7 +164,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
TerminalChatContextKeys.responseContainsCodeBlock,
TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate()
),
@ -196,7 +195,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
TerminalChatContextKeys.responseContainsMultipleCodeBlocks
),
icon: Codicon.play,
@ -227,7 +225,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
TerminalChatContextKeys.responseContainsCodeBlock,
TerminalChatContextKeys.responseContainsMultipleCodeBlocks.negate()
),
@ -259,7 +256,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
TerminalChatContextKeys.responseContainsMultipleCodeBlocks
),
keybinding: {
@ -289,7 +285,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
),
icon: Codicon.commentDiscussion,
menu: [{
@ -319,7 +314,6 @@ registerActiveXtermAction({
precondition: ContextKeyExpr.and(
ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated),
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.agentRegistered,
CTX_INLINE_CHAT_EMPTY.negate()
),
icon: Codicon.send,
@ -348,7 +342,6 @@ registerActiveXtermAction({
title: localize2('cancelChat', 'Cancel Chat'),
precondition: ContextKeyExpr.and(
TerminalChatContextKeys.requestActive,
TerminalChatContextKeys.agentRegistered
),
icon: Codicon.debugStop,
menu: {
@ -366,25 +359,39 @@ registerActiveXtermAction({
});
registerActiveXtermAction({
id: TerminalChatCommandId.FeedbackReportIssue,
title: localize2('reportIssue', 'Report Issue'),
precondition: ContextKeyExpr.and(
TerminalChatContextKeys.requestActive.negate(),
TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined),
TerminalChatContextKeys.responseSupportsIssueReporting
),
icon: Codicon.report,
menu: [{
id: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK,
when: ContextKeyExpr.and(TerminalChatContextKeys.responseContainsCodeBlock.notEqualsTo(undefined), TerminalChatContextKeys.responseSupportsIssueReporting),
group: 'inline',
order: 3
}],
id: TerminalChatCommandId.PreviousFromHistory,
title: localize2('previousFromHitory', 'Previous From History'),
precondition: TerminalChatContextKeys.focused,
keybinding: {
when: TerminalChatContextKeys.focused,
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.UpArrow,
},
run: (_xterm, _accessor, activeInstance) => {
if (isDetachedTerminalInstance(activeInstance)) {
return;
}
const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance);
contr?.acceptFeedback();
contr?.populateHistory(true);
}
});
registerActiveXtermAction({
id: TerminalChatCommandId.NextFromHistory,
title: localize2('nextFromHitory', 'Next From History'),
precondition: TerminalChatContextKeys.focused,
keybinding: {
when: TerminalChatContextKeys.focused,
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyCode.DownArrow,
},
run: (_xterm, _accessor, activeInstance) => {
if (isDetachedTerminalInstance(activeInstance)) {
return;
}
const contr = TerminalChatController.activeChatWidget || TerminalChatController.get(activeInstance);
contr?.populateHistory(false);
}
});

View file

@ -4,26 +4,27 @@
*--------------------------------------------------------------------------------------------*/
import type { Terminal as RawXtermTerminal } from '@xterm/xterm';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy';
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { GeneratingPhrase, IChatAccessibilityService, IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatAgentLocation, IChatAgentRequest, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatUserAction, IChatProgress, IChatService, ChatAgentVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatCodeBlockContextProviderService, showChatView } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatProgress, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal, isDetachedTerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager';
import { ITerminalProcessManager } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalChatWidget } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { ChatModel, ChatRequestModel, IChatRequestVariableData, IChatResponseModel, getHistoryEntriesFromModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatModel, IChatResponseModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat';
import { IViewsService } from 'vs/workbench/services/views/common/viewsService';
import { DeferredPromise } from 'vs/base/common/async';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { assertType } from 'vs/base/common/types';
import { CancelablePromise, createCancelablePromise, DeferredPromise } from 'vs/base/common/async';
import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
const enum Message {
NONE = 0,
@ -48,6 +49,9 @@ export class TerminalChatController extends Disposable implements ITerminalContr
*/
static activeChatWidget?: TerminalChatController;
private static _storageKey = 'terminal-inline-chat-history';
private static _promptHistory: string[] = [];
/**
* The chat widget for the controller, this is lazy as we don't want to instantiate it until
* both it's required and xterm is ready.
@ -61,17 +65,11 @@ export class TerminalChatController extends Disposable implements ITerminalContr
get chatWidget(): TerminalChatWidget | undefined { return this._chatWidget?.value; }
private readonly _requestActiveContextKey: IContextKey<boolean>;
private readonly _terminalAgentRegisteredContextKey: IContextKey<boolean>;
private readonly _responseContainsCodeBlockContextKey: IContextKey<boolean>;
private readonly _responseContainsMulitpleCodeBlocksContextKey: IContextKey<boolean>;
private readonly _responseSupportsIssueReportingContextKey: IContextKey<boolean>;
private readonly _sessionResponseVoteContextKey: IContextKey<string | undefined>;
private _messages = this._store.add(new Emitter<Message>());
private _currentRequest: ChatRequestModel | undefined;
private _lastInput: string | undefined;
private _lastResponseContent: string | undefined;
get lastResponseContent(): string | undefined {
return this._lastResponseContent;
@ -81,7 +79,6 @@ export class TerminalChatController extends Disposable implements ITerminalContr
get onDidHide() { return this.chatWidget?.onDidHide ?? Event.None; }
private _terminalAgentName = 'terminal';
private _terminalAgentId: string | undefined;
private readonly _model: MutableDisposable<ChatModel> = this._register(new MutableDisposable());
@ -89,31 +86,32 @@ export class TerminalChatController extends Disposable implements ITerminalContr
return this._chatWidget?.value.inlineChatWidget.scopedContextKeyService ?? this._contextKeyService;
}
private _sessionCtor: CancelablePromise<void> | undefined;
private _historyOffset: number = -1;
private _historyCandidate: string = '';
private _historyUpdate: (prompt: string) => void;
private _currentRequestId: string | undefined;
private _activeRequestCts?: CancellationTokenSource;
constructor(
private readonly _instance: ITerminalInstance,
processManager: ITerminalProcessManager,
widgetManager: TerminalWidgetManager,
@ITerminalService private readonly _terminalService: ITerminalService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService,
@IChatService private readonly _chatService: IChatService,
@IChatCodeBlockContextProviderService private readonly _chatCodeBlockContextProviderService: IChatCodeBlockContextProviderService,
@IViewsService private readonly _viewsService: IViewsService,
@IStorageService private readonly _storageService: IStorageService,
) {
super();
this._requestActiveContextKey = TerminalChatContextKeys.requestActive.bindTo(this._contextKeyService);
this._terminalAgentRegisteredContextKey = TerminalChatContextKeys.agentRegistered.bindTo(this._contextKeyService);
this._responseContainsCodeBlockContextKey = TerminalChatContextKeys.responseContainsCodeBlock.bindTo(this._contextKeyService);
this._responseContainsMulitpleCodeBlocksContextKey = TerminalChatContextKeys.responseContainsMultipleCodeBlocks.bindTo(this._contextKeyService);
this._responseSupportsIssueReportingContextKey = TerminalChatContextKeys.responseSupportsIssueReporting.bindTo(this._contextKeyService);
this._sessionResponseVoteContextKey = TerminalChatContextKeys.sessionResponseVote.bindTo(this._contextKeyService);
if (!this.initTerminalAgent()) {
this._register(this._chatAgentService.onDidChangeAgents(() => this.initTerminalAgent()));
}
this._register(this._chatCodeBlockContextProviderService.registerProvider({
getCodeBlockContext: (editor) => {
if (!editor || !this._chatWidget?.hasValue || !this.hasFocus()) {
@ -128,34 +126,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr
}
}, 'terminal'));
// TODO
// This is glue/debt that's needed while ChatModel isn't yet adopted. The chat model uses
// a default chat model (unless configured) and feedback is reported against that one. This
// code forwards the feedback to an actual registered provider
this._register(this._chatService.onDidPerformUserAction(e => {
// only forward feedback from the inline chat widget default model
if (
this._chatWidget?.rawValue?.inlineChatWidget.usesDefaultChatModel
&& e.sessionId === this._chatWidget?.rawValue?.inlineChatWidget.getChatModel().sessionId
) {
if (e.action.kind === 'bug') {
this.acceptFeedback(undefined);
} else if (e.action.kind === 'vote') {
this.acceptFeedback(e.action.direction === ChatAgentVoteDirection.Up);
}
TerminalChatController._promptHistory = JSON.parse(this._storageService.get(TerminalChatController._storageKey, StorageScope.PROFILE, '[]'));
this._historyUpdate = (prompt: string) => {
const idx = TerminalChatController._promptHistory.indexOf(prompt);
if (idx >= 0) {
TerminalChatController._promptHistory.splice(idx, 1);
}
}));
}
private initTerminalAgent(): boolean {
const terminalAgent = this._chatAgentService.getAgentsByName(this._terminalAgentName)[0];
if (terminalAgent) {
this._terminalAgentId = terminalAgent.id;
this._terminalAgentRegisteredContextKey.set(true);
return true;
}
return false;
TerminalChatController._promptHistory.unshift(prompt);
this._historyOffset = -1;
this._historyCandidate = '';
this._storageService.store(TerminalChatController._storageKey, JSON.stringify(TerminalChatController._promptHistory), StorageScope.PROFILE, StorageTarget.USER);
};
}
xtermReady(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void {
@ -178,41 +159,17 @@ export class TerminalChatController extends Disposable implements ITerminalContr
});
}
acceptFeedback(helpful?: boolean): void {
const model = this._model.value;
if (!this._currentRequest || !model) {
return;
}
let action: ChatUserAction;
if (helpful === undefined) {
action = { kind: 'bug' };
} else {
this._sessionResponseVoteContextKey.set(helpful ? 'up' : 'down');
action = { kind: 'vote', direction: helpful ? ChatAgentVoteDirection.Up : ChatAgentVoteDirection.Down };
}
// TODO:extract into helper method
for (const request of model.getRequests()) {
if (request.response?.response.value || request.response?.result) {
this._chatService.notifyUserAction({
sessionId: request.session.sessionId,
requestId: request.id,
agentId: request.response?.agent?.id,
result: request.response?.result,
action
});
}
}
this._chatWidget?.value.inlineChatWidget.updateStatus('Thank you for your feedback!', { resetAfter: 1250 });
}
private async _createSession(): Promise<void> {
this._sessionCtor = createCancelablePromise<void>(async token => {
if (!this._model.value) {
this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, token);
cancel(): void {
if (this._currentRequest) {
this._model.value?.cancelRequest(this._currentRequest);
}
this._requestActiveContextKey.set(false);
this._chatWidget?.value.inlineChatWidget.updateProgress(false);
this._chatWidget?.value.inlineChatWidget.updateInfo('');
this._chatWidget?.value.inlineChatWidget.updateToolbar(true);
if (!this._model.value) {
throw new Error('Failed to start chat session');
}
}
});
this._register(toDisposable(() => this._sessionCtor?.cancel()));
}
private _forcedPlaceholder: string | undefined = undefined;
@ -239,112 +196,65 @@ export class TerminalChatController extends Disposable implements ITerminalContr
}
clear(): void {
if (this._currentRequest) {
this._model.value?.cancelRequest(this._currentRequest);
}
this.cancel();
this._model.clear();
this._chatWidget?.rawValue?.hide();
this._chatWidget?.rawValue?.setValue(undefined);
this._responseContainsCodeBlockContextKey.reset();
this._sessionResponseVoteContextKey.reset();
this._requestActiveContextKey.reset();
this._chatWidget?.value.hide();
this._chatWidget?.value.setValue(undefined);
}
async acceptInput(): Promise<IChatResponseModel | undefined> {
if (!this._model.value) {
this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, CancellationToken.None);
if (!this._model.value) {
throw new Error('Could not start chat session');
}
}
this._messages.fire(Message.ACCEPT_INPUT);
const model = this._model.value;
this._lastInput = this._chatWidget?.value?.input();
if (!this._lastInput) {
assertType(this._chatWidget);
assertType(this._model.value);
const lastInput = this._chatWidget.value.inlineChatWidget.value;
if (!lastInput) {
return;
}
const responseCreated = new DeferredPromise<IChatResponseModel>();
let responseCreatedComplete = false;
const completeResponseCreated = () => {
if (!responseCreatedComplete && this._currentRequest?.response) {
responseCreated.complete(this._currentRequest.response);
responseCreatedComplete = true;
}
};
const accessibilityRequestId = this._chatAccessibilityService.acceptRequest();
const model = this._model.value;
this._chatWidget.value.inlineChatWidget.setChatModel(model);
this._historyUpdate(lastInput);
this._activeRequestCts?.cancel();
this._activeRequestCts = new CancellationTokenSource();
const store = new DisposableStore();
this._requestActiveContextKey.set(true);
const cancellationToken = new CancellationTokenSource().token;
let responseContent = '';
const progressCallback = (progress: IChatProgress) => {
if (cancellationToken.isCancellationRequested) {
return;
}
if (progress.kind === 'markdownContent') {
responseContent += progress.content.value;
}
if (this._currentRequest) {
model.acceptResponseProgress(this._currentRequest, progress);
completeResponseCreated();
}
};
await model.waitForInitialization();
this._chatWidget?.value.addToHistory(this._lastInput);
const request: IParsedChatRequest = {
text: this._lastInput,
parts: []
};
const requestVarData: IChatRequestVariableData = {
variables: []
};
this._currentRequest = model.addRequest(request, requestVarData, 0);
completeResponseCreated();
const requestProps: IChatAgentRequest = {
sessionId: model.sessionId,
requestId: this._currentRequest!.id,
agentId: this._terminalAgentId!,
message: this._lastInput,
variables: { variables: [] },
location: ChatAgentLocation.Terminal
};
const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput);
this._currentRequestId = response?.requestId;
const responsePromise = new DeferredPromise<IChatResponseModel | undefined>();
try {
const task = this._chatAgentService.invokeAgent(this._terminalAgentId!, requestProps, progressCallback, getHistoryEntriesFromModel(model, this._terminalAgentId!), cancellationToken);
this._chatWidget?.value.inlineChatWidget.updateChatMessage(undefined);
this._chatWidget?.value.inlineChatWidget.updateProgress(true);
this._chatWidget?.value.inlineChatWidget.updateInfo(GeneratingPhrase + '\u2026');
await task;
} catch (e) {
this._requestActiveContextKey.set(true);
if (response) {
store.add(response.onDidChange(async () => {
responseContent += response.response.value;
this._chatWidget?.value.inlineChatWidget.updateProgress(true);
if (response.isCanceled) {
this._requestActiveContextKey.set(false);
responsePromise.complete(undefined);
return;
}
if (response.isComplete) {
this._requestActiveContextKey.set(false);
this._requestActiveContextKey.set(false);
const containsCode = responseContent.includes('```');
this._chatWidget!.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: response!.requestId }, false, containsCode);
const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0);
const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1);
this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock);
this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock);
this._chatWidget?.value.inlineChatWidget.updateToolbar(true);
this._chatWidget?.value.inlineChatWidget.updateProgress(false);
responsePromise.complete(response);
}
}));
}
await responsePromise.p;
return response;
} catch {
return;
} finally {
this._requestActiveContextKey.set(false);
this._chatWidget?.value.inlineChatWidget.updateProgress(false);
this._chatWidget?.value.inlineChatWidget.updateInfo('');
this._chatWidget?.value.inlineChatWidget.updateToolbar(true);
if (this._currentRequest) {
model.completeResponse(this._currentRequest);
completeResponseCreated();
}
this._lastResponseContent = responseContent;
if (this._currentRequest) {
this._chatAccessibilityService.acceptResponse(responseContent, accessibilityRequestId);
const containsCode = responseContent.includes('```');
this._chatWidget?.value.inlineChatWidget.updateChatMessage({ message: new MarkdownString(responseContent), requestId: this._currentRequest.id }, false, containsCode);
const firstCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0);
const secondCodeBlock = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(1);
this._responseContainsCodeBlockContextKey.set(!!firstCodeBlock);
this._responseContainsMulitpleCodeBlocksContextKey.set(!!secondCodeBlock);
this._chatWidget?.value.inlineChatWidget.updateToolbar(true);
}
const supportIssueReporting = this._currentRequest?.response?.agent?.metadata?.supportIssueReporting;
if (supportIssueReporting !== undefined) {
this._responseSupportsIssueReportingContextKey.set(supportIssueReporting);
}
store.dispose();
}
return responseCreated.p;
}
updateInput(text: string, selectAll = true): void {
@ -369,6 +279,47 @@ export class TerminalChatController extends Disposable implements ITerminalContr
return !!this._chatWidget?.rawValue?.hasFocus() ?? false;
}
populateHistory(up: boolean) {
if (!this._chatWidget?.value) {
return;
}
const len = TerminalChatController._promptHistory.length;
if (len === 0) {
return;
}
if (this._historyOffset === -1) {
// remember the current value
this._historyCandidate = this._chatWidget.value.inlineChatWidget.value;
}
const newIdx = this._historyOffset + (up ? 1 : -1);
if (newIdx >= len) {
// reached the end
return;
}
let entry: string;
if (newIdx < 0) {
entry = this._historyCandidate;
this._historyOffset = -1;
} else {
entry = TerminalChatController._promptHistory[newIdx];
this._historyOffset = newIdx;
}
this._chatWidget.value.inlineChatWidget.value = entry;
this._chatWidget.value.inlineChatWidget.selectAll();
}
cancel(): void {
this._sessionCtor?.cancel();
this._sessionCtor = undefined;
this._activeRequestCts?.cancel();
this._requestActiveContextKey.set(false);
}
async acceptCommand(shouldExecute: boolean): Promise<void> {
const code = await this.chatWidget?.inlineChatWidget.getCodeBlockInfo(0);
if (!code) {
@ -377,18 +328,22 @@ export class TerminalChatController extends Disposable implements ITerminalContr
this._chatWidget?.value.acceptCommand(code.textEditorModel.getValue(), shouldExecute);
}
reveal(): void {
async reveal(): Promise<void> {
await this._createSession();
this._chatWidget?.value.reveal();
this._chatWidget?.value.focus();
}
async viewInChat(): Promise<void> {
//TODO: is this necessary? better way?
const widget = await showChatView(this._viewsService);
const request = this._currentRequest;
if (!widget || !request?.response) {
const currentRequest = this.chatWidget?.inlineChatWidget.chatWidget.viewModel?.model.getRequests().find(r => r.id === this._currentRequestId);
if (!widget || !currentRequest?.response) {
return;
}
const message: IChatProgress[] = [];
for (const item of request.response.response.value) {
for (const item of currentRequest.response.response.value) {
if (item.kind === 'textEditGroup') {
for (const group of item.edits) {
message.push({
@ -404,24 +359,15 @@ export class TerminalChatController extends Disposable implements ITerminalContr
this._chatService.addCompleteRequest(widget!.viewModel!.sessionId,
// DEBT: Add hardcoded agent name until its removed
`@${this._terminalAgentName} ${request.message.text}`,
request.variableData,
request.attempt,
`@${this._terminalAgentName} ${currentRequest.message.text}`,
currentRequest.variableData,
currentRequest.attempt,
{
message,
result: request.response!.result,
followups: request.response!.followups
result: currentRequest.response!.result,
followups: currentRequest.response!.followups
});
widget.focusLastMessage();
this._chatWidget?.rawValue?.hide();
}
// TODO: Move to register calls, don't override
override dispose() {
if (this._currentRequest) {
this._model.value?.cancelRequest(this._currentRequest);
}
super.dispose();
this.clear();
}
}

View file

@ -16,7 +16,7 @@ import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService';
import { InlineChatWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
import { ITerminalInstance, type IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal';
import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_FEEDBACK, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat';
import { MENU_TERMINAL_CHAT_INPUT, MENU_TERMINAL_CHAT_WIDGET, MENU_TERMINAL_CHAT_WIDGET_STATUS, TerminalChatCommandId, TerminalChatContextKeys } from 'vs/workbench/contrib/terminalContrib/chat/browser/terminalChat';
import { TerminalStickyScrollContribution } from 'vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollContribution';
const enum Constants {
@ -72,7 +72,6 @@ export class TerminalChatWidget extends Disposable {
}
}
},
feedbackMenuId: MENU_TERMINAL_CHAT_WIDGET_FEEDBACK,
telemetrySource: 'terminal-inline-chat',
rendererOptions: { editableCodeBlock: true }
}
@ -80,6 +79,7 @@ export class TerminalChatWidget extends Disposable {
this._register(Event.any(
this._inlineChatWidget.onDidChangeHeight,
this._instance.onDimensionsChanged,
this._inlineChatWidget.chatWidget.onDidChangeContentHeight,
Event.debounce(this._xterm.raw.onCursorMove, () => void 0, MicrotaskDelay),
)(() => this._relayout()));
@ -155,11 +155,15 @@ export class TerminalChatWidget extends Disposable {
if (!terminalWrapperHeight) {
return;
}
if (top > terminalWrapperHeight - widgetHeight) {
if (top > terminalWrapperHeight - widgetHeight && terminalWrapperHeight - widgetHeight > 0) {
this._setTerminalOffset(top - (terminalWrapperHeight - widgetHeight));
} else {
this._setTerminalOffset(undefined);
}
if (terminalWrapperHeight - widgetHeight < 0) {
this._dimension = new Dimension(this._dimension!.width, terminalWrapperHeight - top - 20);
this._inlineChatWidget.layout(this._dimension!);
}
}
private _getTerminalWrapperHeight(): number | undefined {