Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare inline chat for context (variables, tools, etc) #221456

Merged
merged 4 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading