Inline chat fixes (#208383)

* - Allow to set overflowWidgetDomNode for chat widget's input editor
- tweak z-index for inline chat content widget

fixes https://github.com/microsoft/vscode/issues/208375

* set default/dummy model while chat is getting ready

fixes https://github.com/microsoft/vscode/issues/208377

* restore decoration highlights for commands, tweak command suggestions, leave some debt for agents

fixes https://github.com/microsoft/vscode/issues/208379
This commit is contained in:
Johannes Rieken 2024-03-22 09:58:16 +01:00 committed by GitHub
parent 85eea4a9b2
commit fcb8417ba3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 105 additions and 124 deletions

View file

@ -98,6 +98,7 @@ export interface IChatWidgetViewOptions {
inputSideToolbar?: MenuId;
telemetrySource?: string;
};
editorOverflowWidgetsDomNode?: HTMLElement;
}
export interface IChatViewViewContext {

View file

@ -47,11 +47,23 @@ import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IChatHistoryEntry, IChatWidgetHistoryService } from 'vs/workbench/contrib/chat/common/chatWidgetHistoryService';
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
const $ = dom.$;
const INPUT_EDITOR_MAX_HEIGHT = 250;
interface IChatInputPartOptions {
renderFollowups: boolean;
renderStyle?: 'default' | 'compact';
menus: {
executeToolbar: MenuId;
inputSideToolbar?: MenuId;
telemetrySource?: string;
};
editorOverflowWidgetsDomNode?: HTMLElement;
}
export class ChatInputPart extends Disposable implements IHistoryNavigationWidget {
static readonly INPUT_SCHEME = 'chatSessionInput';
private static _counter = 0;
@ -120,11 +132,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
constructor(
// private readonly editorOptions: ChatEditorOptions, // TODO this should be used
private readonly location: ChatAgentLocation,
private readonly options: {
renderFollowups: boolean;
renderStyle?: 'default' | 'compact';
menus: { executeToolbar: MenuId; inputSideToolbar?: MenuId; telemetrySource?: string };
},
private readonly options: IChatInputPartOptions,
@IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService,
@IModelService private readonly modelService: IModelService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ -273,7 +281,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement;
this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement;
const options = getSimpleEditorOptions(this.configurationService);
const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService);
options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode;
options.readOnly = false;
options.ariaLabel = this._getAriaLabel();
options.fontFamily = DEFAULT_FONT_FAMILY;

View file

@ -1,11 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.chat-slash-command-content-widget {
background-color: var(--vscode-chat-slashCommandBackground);
color: var(--vscode-chat-slashCommandForeground);
border-radius: 3px;
padding: 1px;
}

View file

@ -1,96 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./chatSlashCommandContentWidget';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Range } from 'vs/editor/common/core/range';
import { Disposable } from 'vs/base/common/lifecycle';
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget } from 'vs/editor/browser/editorBrowser';
import { KeyCode } from 'vs/base/common/keyCodes';
import { localize } from 'vs/nls';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
export class SlashCommandContentWidget extends Disposable implements IContentWidget {
private _domNode = document.createElement('div');
private _lastSlashCommandText: string | undefined;
private _isVisible = false;
constructor(private _editor: ICodeEditor) {
super();
this._domNode.toggleAttribute('hidden', true);
this._domNode.classList.add('chat-slash-command-content-widget');
// If backspace at a slash command boundary, remove the slash command
this._register(this._editor.onKeyDown((e) => this._handleKeyDown(e)));
}
override dispose() {
this.hide();
super.dispose();
}
show() {
if (!this._isVisible) {
this._isVisible = true;
this._domNode.toggleAttribute('hidden', false);
this._editor.addContentWidget(this);
}
}
hide() {
if (this._isVisible) {
this._isVisible = false;
this._domNode.toggleAttribute('hidden', true);
this._editor.removeContentWidget(this);
}
}
setCommandText(slashCommand: string) {
this._domNode.innerText = `/${slashCommand} `;
this._lastSlashCommandText = slashCommand;
}
getId() {
return 'chat-slash-command-content-widget';
}
getDomNode() {
return this._domNode;
}
getPosition() {
return { position: { lineNumber: 1, column: 1 }, preference: [ContentWidgetPositionPreference.EXACT] };
}
beforeRender(): null {
const lineHeight = this._editor.getOption(EditorOption.lineHeight);
this._domNode.style.lineHeight = `${lineHeight - 2 /*padding*/}px`;
return null;
}
private _handleKeyDown(e: IKeyboardEvent) {
if (e.keyCode !== KeyCode.Backspace) {
return;
}
const firstLine = this._editor.getModel()?.getLineContent(1);
const selection = this._editor.getSelection();
const withSlash = `/${this._lastSlashCommandText} `;
if (!firstLine?.startsWith(withSlash) || !selection?.isEmpty() || selection?.startLineNumber !== 1 || selection?.startColumn !== withSlash.length + 1) {
return;
}
// Allow to undo the backspace
this._editor.executeEdits('chat-slash-command', [{
range: new Range(1, 1, 1, selection.startColumn),
text: null
}]);
// Announce the deletion
aria.alert(localize('exited slash command mode', 'Exited {0} mode', this._lastSlashCommandText));
}
}

View file

@ -501,7 +501,8 @@ export class ChatWidget extends Disposable implements IChatWidget {
{
renderFollowups: options?.renderFollowups ?? true,
renderStyle: options?.renderStyle,
menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }
menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus },
editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,
}
));
this.inputPart.render(container, '', this);

View file

@ -18,6 +18,7 @@ import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSessi
import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { ChatAgentLocation } from 'vs/workbench/contrib/chat/common/chatAgents';
import { editorBackground, editorForeground, inputBackground } from 'vs/platform/theme/common/colorRegistry';
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
export class InlineChatContentWidget implements IContentWidget {
@ -36,6 +37,8 @@ export class InlineChatContentWidget implements IContentWidget {
private _visible: boolean = false;
private _focusNext: boolean = false;
private readonly _defaultChatModel: ChatModel;
private readonly _widget: ChatWidget;
constructor(
@ -43,11 +46,14 @@ export class InlineChatContentWidget implements IContentWidget {
@IInstantiationService instaService: IInstantiationService,
) {
this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, `inlineChatDefaultModel/editorContentWidgetPlaceholder`, undefined));
this._widget = instaService.createInstance(
ChatWidget,
ChatAgentLocation.Editor,
{ resource: true },
{
editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(),
renderStyle: 'compact',
renderInputOnTop: true,
supportsFileReferences: false,
@ -66,6 +72,7 @@ export class InlineChatContentWidget implements IContentWidget {
this._store.add(this._widget);
this._store.add(this._widget.onDidChangeHeight(() => _editor.layoutContentWidget(this)));
this._widget.render(this._inputContainer);
this._widget.setModel(this._defaultChatModel, {});
this._domNode.tabIndex = -1;
this._domNode.className = 'inline-chat-content-widget interactive-session';
@ -78,7 +85,9 @@ export class InlineChatContentWidget implements IContentWidget {
const tracker = dom.trackFocus(this._domNode);
this._store.add(tracker.onDidBlur(() => {
if (this._visible) {
if (this._visible
// && !"ON"
) {
this._onDidBlur.fire();
}
}));

View file

@ -19,7 +19,7 @@ import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IPosition, Position } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { ISelection, Selection } from 'vs/editor/common/core/selection';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { CompletionItemKind, CompletionList, TextEdit } from 'vs/editor/common/languages';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { InlineCompletionsController } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsController';
@ -41,7 +41,7 @@ import { InlineChatZoneWidget } from './inlineChatZoneWidget';
import { CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_LAST_FEEDBACK, CTX_INLINE_CHAT_RESPONSE_TYPES, CTX_INLINE_CHAT_SUPPORT_ISSUE_REPORTING, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseFeedbackKind, InlineChatResponseTypes } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { StashedSession } from './inlineChatSession';
import { IValidEditOperation } from 'vs/editor/common/model';
import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from 'vs/editor/common/model';
import { InlineChatContentWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatContentWidget';
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import { tail } from 'vs/base/common/arrays';
@ -432,7 +432,11 @@ export class InlineChatController implements IEditorContribution {
}
}));
// TODO@jrieken
// Update context key
this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false);
// #region DEBT
// DEBT@jrieken
// REMOVE when agents are adopted
this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
_debugDisplayName: 'inline chat commands',
@ -455,14 +459,63 @@ export class InlineChatController implements IEditorContribution {
kind: CompletionItemKind.Text,
insertText: withSlash,
range: Range.fromPositions(new Position(1, 1), position),
command: command.executeImmediately ? { id: 'workbench.action.chat.acceptInput', title: withSlash } : undefined
});
}
return result;
}
}));
// Update context key
this._ctxSupportIssueReporting.set(this._session.provider.supportIssueReporting ?? false);
const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => {
const newDecorations: IModelDeltaDecoration[] = [];
for (const command of (this._session?.session.slashCommands ?? []).sort((a, b) => b.command.length - a.command.length)) {
const withSlash = `/${command.command}`;
const firstLine = model.getLineContent(1);
if (firstLine.startsWith(withSlash)) {
newDecorations.push({
range: new Range(1, 1, 1, withSlash.length + 1),
options: {
description: 'inline-chat-slash-command',
inlineClassName: 'inline-chat-slash-command',
after: {
// Force some space between slash command and placeholder
content: ' '
}
}
});
// inject detail when otherwise empty
if (firstLine.trim() === `/${command.command}`) {
newDecorations.push({
range: new Range(1, withSlash.length, 1, withSlash.length),
options: {
description: 'inline-chat-slash-command-detail',
after: {
content: `${command.detail}`,
inlineClassName: 'inline-chat-slash-command-detail'
}
}
});
}
break;
}
}
collection.set(newDecorations);
};
const inputInputEditor = this._input.value.chatWidget.inputEditor;
const zoneInputEditor = this._zone.value.widget.chatWidget.inputEditor;
const inputDecorations = inputInputEditor.createDecorationsCollection();
const zoneDecorations = zoneInputEditor.createDecorationsCollection();
this._sessionStore.add(inputInputEditor.onDidChangeModelContent(() => updateSlashDecorations(inputDecorations, inputInputEditor.getModel()!)));
this._sessionStore.add(zoneInputEditor.onDidChangeModelContent(() => updateSlashDecorations(zoneDecorations, zoneInputEditor.getModel()!)));
this._sessionStore.add(toDisposable(() => {
inputDecorations.clear();
zoneDecorations.clear();
}));
//#endregion ------- DEBT
if (!this._session.lastExchange) {
return State.WAIT_FOR_INPUT;

View file

@ -93,6 +93,8 @@ export interface IInlineChatWidgetConstructionOptions {
* globally.
*/
editableCodeBlocks?: boolean;
editorOverflowWidgetsDomNode?: HTMLElement;
}
export interface IInlineChatMessage {
@ -395,9 +397,18 @@ export class InlineChatWidget {
this._chatWidget.setInput(value);
}
selectAll(includeSlashCommand: boolean = true) {
// TODO@jrieken includeSlashCommand
this._chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
// DEBT@jrieken
// REMOVE when agents are adopted
let startColumn = 1;
if (!includeSlashCommand) {
const match = /^(\/\w+)\s*/.exec(this._chatWidget.inputEditor.getModel()!.getLineContent(1));
if (match) {
startColumn = match[1].length + 1;
}
}
this._chatWidget.inputEditor.setSelection(new Selection(1, startColumn, Number.MAX_SAFE_INTEGER, 1));
}
set placeholder(value: string) {
@ -612,7 +623,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget {
@ITextModelService textModelResolverService: ITextModelService,
@IChatService chatService: IChatService,
) {
super(ChatAgentLocation.Editor, options, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService);
super(ChatAgentLocation.Editor, { ...options, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService);
// preview editors
this._previewDiffEditor = new Lazy(() => this._store.add(instantiationService.createInstance(EmbeddedDiffEditorWidget, this._elements.previewDiff, {

View file

@ -266,7 +266,10 @@
}
.monaco-workbench .inline-chat-slash-command {
opacity: 0;
background-color: var(--vscode-chat-slashCommandBackground);
color: var(--vscode-chat-slashCommandForeground);
border-radius: 4px;
padding: 1px;
}
.monaco-workbench .inline-chat-slash-command-detail {

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .inline-chat-content-widget {
z-index: 50;
padding: 6px 6px 2px 6px;
border-radius: 4px;
background-color: var(--vscode-inlineChat-background);