Skip to content

Commit

Permalink
Prepare inline chat for context (variables, tools, etc) (#221456)
Browse files Browse the repository at this point in the history
* Show attach context action depending on the `chat.experimental.variables.XYZ` setting

re microsoft/vscode-copilot#6622

* set `supportsFileReferences`-flag based on `chat.experimental.variables.XYZ` setting

* inline chat content widget should not hide when it has been interacted with, should have a width defined

fyi @joyceerhl this changes things around attached context and its event, also works around #221385

microsoft/vscode-copilot#6622

* hide #selection and #editor when in editor inline chat

the implementation is quite ugly and needs some debt removal, for now the names and location are hardcoded
  • Loading branch information
jrieken authored Jul 11, 2024
1 parent 8d13b31 commit 05db2bf
Show file tree
Hide file tree
Showing 11 changed files with 68 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,29 @@ class AttachContextAction extends Action2 {

static readonly ID = 'workbench.action.chat.attachContext';

// used to enable/disable the keybinding and defined menu containment
private static _cdt = ContextKeyExpr.or(
ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)),
ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Editor), ContextKeyExpr.equals('config.chat.experimental.variables.editor', true)),
ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Notebook), ContextKeyExpr.equals('config.chat.experimental.variables.notebook', true)),
ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Terminal), ContextKeyExpr.equals('config.chat.experimental.variables.terminal', true)),
);

constructor() {
super({
id: AttachContextAction.ID,
title: localize2('workbench.action.chat.attachContext.label', "Attach Context"),
icon: Codicon.attach,
category: CHAT_CATEGORY,
precondition: AttachContextAction._cdt,
keybinding: {
when: CONTEXT_IN_CHAT_INPUT,
primary: KeyMod.CtrlCmd | KeyCode.Slash,
weight: KeybindingWeight.EditorContrib
},
menu: [
{
when: ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(ChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)),
when: AttachContextAction._cdt,
id: MenuId.ChatExecute,
group: 'navigation',
},
Expand Down Expand Up @@ -250,7 +259,7 @@ class AttachContextAction extends Action2 {
const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart);
const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true;
const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = [];
for (const variable of chatVariablesService.getVariables()) {
for (const variable of chatVariablesService.getVariables(widget.location)) {
if (variable.fullName && (!variable.isSlow || slowSupported)) {
quickPickItems.push({
label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`,
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
}

const variables = [
...chatVariablesService.getVariables(),
...chatVariablesService.getVariables(ChatAgentLocation.Panel),
{ name: 'file', description: nls.localize('file', "Choose a file in the workspace") }
];
const variableText = variables
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/browser/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export interface IChatWidget {
readonly onDidHide: Event<void>;
readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>;
readonly onDidChangeParsedInput: Event<void>;
readonly onDidDeleteContext: Event<IChatRequestVariableEntry>;
readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>;
readonly location: ChatAgentLocation;
readonly viewContext: IChatWidgetViewContext;
readonly viewModel: IChatViewModel | undefined;
Expand Down
28 changes: 19 additions & 9 deletions src/vs/workbench/contrib/chat/browser/chatInputPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private _onDidBlur = this._register(new Emitter<void>());
readonly onDidBlur = this._onDidBlur.event;

private _onDidDeleteContext = this._register(new Emitter<IChatRequestVariableEntry>());
readonly onDidDeleteContext = this._onDidDeleteContext.event;
private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>());
readonly onDidChangeContext = this._onDidChangeContext.event;

private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>());
readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event;

public get attachedContext() {
public get attachedContext(): ReadonlySet<IChatRequestVariableEntry> {
return this._attachedContext;
}

Expand Down Expand Up @@ -339,12 +339,22 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this._inputEditor.focus();
}

attachContext(...contentReferences: IChatRequestVariableEntry[]): void {
for (const reference of contentReferences) {
this.attachedContext.add(reference);
clearContext(): void {
if (this._attachedContext.size > 0) {
const removed = Array.from(this._attachedContext);
this._attachedContext.clear();
this._onDidChangeContext.fire({ removed });
}
}

this.initAttachedContext(this.attachedContextContainer);
attachContext(contentReferences: IChatRequestVariableEntry[]): void {
if (contentReferences.length > 0) {
for (const reference of contentReferences) {
this._attachedContext.add(reference);
}
this.initAttachedContext(this.attachedContextContainer);
this._onDidChangeContext.fire({ added: contentReferences });
}
}

render(container: HTMLElement, initialValue: string, widget: IChatWidget) {
Expand Down Expand Up @@ -521,7 +531,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.attachedContextDisposables.add(clearButton);
clearButton.icon = Codicon.close;
const disp = clearButton.onDidClick((e) => {
this.attachedContext.delete(attachment);
this._attachedContext.delete(attachment);
disp.dispose();

// Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click)
Expand All @@ -533,7 +543,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}

this._onDidChangeHeight.fire();
this._onDidDeleteContext.fire(attachment);
this._onDidChangeContext.fire({ removed: [attachment] });
});
this.attachedContextDisposables.add(disp);
});
Expand Down
7 changes: 5 additions & 2 deletions src/vs/workbench/contrib/chat/browser/chatVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ export class ChatVariablesService implements IChatVariablesService {
return this._resolver.get(name.toLowerCase())?.data;
}

getVariables(): Iterable<Readonly<IChatVariableData>> {
getVariables(location: ChatAgentLocation): Iterable<Readonly<IChatVariableData>> {
const all = Iterable.map(this._resolver.values(), data => data.data);
return Iterable.filter(all, data => !data.hidden);
return Iterable.filter(all, data => {
// TODO@jrieken this is improper and should be know from the variable registeration data
return location !== ChatAgentLocation.Editor || !new Set(['selection', 'editor']).has(data.name);
});
}

getDynamicVariables(sessionId: string): ReadonlyArray<IDynamicVariable> {
Expand Down
12 changes: 6 additions & 6 deletions src/vs/workbench/contrib/chat/browser/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ export class ChatWidget extends Disposable implements IChatWidget {
private _onDidAcceptInput = this._register(new Emitter<void>());
readonly onDidAcceptInput = this._onDidAcceptInput.event;

private _onDidDeleteContext = this._register(new Emitter<IChatRequestVariableEntry>());
readonly onDidDeleteContext = this._onDidDeleteContext.event;
private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>());
readonly onDidChangeContext = this._onDidChangeContext.event;

private _onDidHide = this._register(new Emitter<void>());
readonly onDidHide = this._onDidHide.event;
Expand Down Expand Up @@ -593,7 +593,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
});
}));
this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire()));
this._register(this.inputPart.onDidDeleteContext((e) => this._onDidDeleteContext.fire(e)));
this._register(this.inputPart.onDidChangeContext((e) => this._onDidChangeContext.fire(e)));
this._register(this.inputPart.onDidAcceptFollowup(e => {
if (!this.viewModel) {
return;
Expand Down Expand Up @@ -775,7 +775,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
});

if (result) {
this.inputPart.attachedContext.clear();
this.inputPart.clearContext();
this.inputPart.acceptInput(isUserQuery);
this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });
this.inputPart.updateState(this.collectInputState());
Expand All @@ -793,9 +793,9 @@ export class ChatWidget extends Disposable implements IChatWidget {

setContext(overwrite: boolean, ...contentReferences: IChatRequestVariableEntry[]) {
if (overwrite) {
this.inputPart.attachedContext.clear();
this.inputPart.clearContext();
}
this.inputPart.attachContext(...contentReferences);
this.inputPart.attachContext(contentReferences);

if (this.bodyDimension) {
this.layout(this.bodyDimension.height, this.bodyDimension.width);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon
constructor(readonly widget: IChatWidget) {
super();

this._register(this.widget.onDidDeleteContext((e) => {
this._removeContext(e);
this._register(this.widget.onDidChangeContext((e) => {
if (e.removed) {
this._removeContext(e.removed);
}
}));

this._register(this.widget.onDidSubmitAgent(() => {
Expand Down Expand Up @@ -67,8 +69,8 @@ export class ChatContextAttachments extends Disposable implements IChatWidgetCon
this._onDidChangeInputState.fire();
}

private _removeContext(attachment: IChatRequestVariableEntry) {
this._attachedContext.delete(attachment);
private _removeContext(attachments: IChatRequestVariableEntry[]) {
attachments.forEach(this._attachedContext.delete, this._attachedContext);
this._onDidChangeInputState.fire();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ class VariableCompletions extends Disposable {
const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true;

const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart);
const variableItems = Array.from(this.chatVariablesService.getVariables())
const variableItems = Array.from(this.chatVariablesService.getVariables(widget.location))
// This doesn't look at dynamic variables like `file`, where multiple makes sense.
.filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name))
.filter(v => !v.isSlow || slowSupported)
Expand Down
3 changes: 1 addition & 2 deletions src/vs/workbench/contrib/chat/common/chatVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export interface IChatVariableData {
description: string;
modelDescription?: string;
isSlow?: boolean;
hidden?: boolean;
canTakeArgument?: boolean;
}

Expand All @@ -44,7 +43,7 @@ export interface IChatVariablesService {
registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable;
hasVariable(name: string): boolean;
getVariable(name: string): IChatVariableData | undefined;
getVariables(): Iterable<Readonly<IChatVariableData>>;
getVariables(location: ChatAgentLocation): Iterable<Readonly<IChatVariableData>>;
getDynamicVariables(sessionId: string): ReadonlyArray<IDynamicVariable>; // should be its own service?
attachContext(name: string, value: string | URI | Location | unknown, location: ChatAgentLocation): void;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class InlineChatContentWidget implements IContentWidget {
renderStyle: 'minimal',
renderInputOnTop: true,
renderFollowups: true,
supportsFileReferences: false,
supportsFileReferences: configurationService.getValue(`chat.experimental.variables.${location.location}`) === true,
menus: {
telemetrySource: 'inlineChat-content',
executeToolbar: MENU_INLINE_CHAT_EXECUTE,
Expand Down Expand Up @@ -120,9 +120,19 @@ export class InlineChatContentWidget implements IContentWidget {
this._domNode.classList.toggle('contents', toolbar.getItemsLength() > 1);
}));

// note when the widget has been interaced with and disable "close on blur" if so
let widgetHasBeenInteractedWith = false;
this._store.add(this._widget.inputEditor.onDidChangeModelContent(() => {
widgetHasBeenInteractedWith ||= this._widget.inputEditor.getModel()?.getValueLength() !== 0;
}));
this._store.add(this._widget.onDidChangeContext(() => {
widgetHasBeenInteractedWith ||= true;
_editor.layoutContentWidget(this);// https://github.com/microsoft/vscode/issues/221385
}));

const tracker = dom.trackFocus(this._domNode);
this._store.add(tracker.onDidBlur(() => {
if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0 && !quickInputService.currentQuickInput) {
if (this._visible && !widgetHasBeenInteractedWith && !quickInputService.currentQuickInput) {
this._onDidBlur.fire();
}
}));
Expand Down Expand Up @@ -156,10 +166,11 @@ export class InlineChatContentWidget implements IContentWidget {
const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5;
const inputEditorHeight = this._widget.contentHeight;

this._widget.layout(Math.min(maxHeight, inputEditorHeight), 390);
const height = Math.min(maxHeight, inputEditorHeight);
const width = 390;
this._widget.layout(height, width);

// const actualHeight = this._widget.inputPartHeight;
// return new dom.Dimension(width, actualHeight);
dom.size(this._domNode, width, null);
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class InlineChatWidget {
renderStyle: 'minimal',
renderInputOnTop: false,
renderFollowups: true,
supportsFileReferences: false,
supportsFileReferences: _configurationService.getValue(`chat.experimental.variables.${location.location}`) === true,
filter: item => !isWelcomeVM(item),
...options.chatWidgetViewOptions
},
Expand Down

0 comments on commit 05db2bf

Please sign in to comment.