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

Add response variables #191349

Merged
merged 3 commits into from
Aug 26, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/workbench/api/browser/mainThreadChatVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class MainThreadChatSlashCommands implements MainThreadChatVariablesShape
}

$registerVariable(handle: number, data: IChatVariableData): void {
const registration = this._chatVariablesService.registerVariable(data, (messageText, token) => {
const registration = this._chatVariablesService.registerVariable(data, (messageText, _arg, _model, token) => {
return this._proxy.$resolveVariable(handle, messageText, token);
});
this._variables.set(handle, registration);
Expand Down
38 changes: 38 additions & 0 deletions src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ export function registerChatTitleActions() {
}
});

registerAction2(class MentionAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.mention',
title: {
value: localize('interactive.mention.label', "Mention"),
original: 'Mention'
},
f1: false,
category: CHAT_CATEGORY,
icon: Codicon.add,
menu: {
id: MenuId.ChatMessageTitle,
group: 'navigation',
order: 3,
when: CONTEXT_RESPONSE
}
});
}

run(accessor: ServicesAccessor, ...args: any[]) {
const item = args[0];
if (!isResponseVM(item)) {
return;
}

const chatWidgetService = accessor.get(IChatWidgetService);
const widget = chatWidgetService.lastFocusedWidget!;
const num = widget.viewModel!.getItems()
.filter(isResponseVM)
.indexOf(item) + 1;
widget.inputEditor.setValue(`${widget.inputEditor.getValue()} @response:${num} `);
const lastLine = widget.inputEditor.getModel()!.getLineCount();
const lastCol = widget.inputEditor.getModel()!.getLineLength(lastLine);
widget.inputEditor.setSelection({ startColumn: lastCol, endColumn: lastCol, startLineNumber: lastLine, endLineNumber: lastLine });
}
});

registerAction2(class InsertToNotebookAction extends Action2 {
constructor() {
super({
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browse
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
import 'vs/workbench/contrib/chat/browser/contrib/chatHistoryVariables';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from 'vs/base/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';

class ChatHistoryVariables extends Disposable {
constructor(
@IChatVariablesService chatVariablesService: IChatVariablesService,
) {
super();

this._register(chatVariablesService.registerVariable({ name: 'response', description: '', canTakeArgument: true, hidden: true }, async (message, arg, model, token) => {
if (!arg) {
return undefined;
}

const responseNum = parseInt(arg, 10);
const response = model.getRequests()[responseNum - 1].response;
if (!response) {
return undefined;
}

return [{ level: 'full', value: response.response.asString() }];
}));
}
}

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ChatHistoryVariables, LifecyclePhase.Eventually);
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workbench/contrib/chat/common/chatColors';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';


const decorationDescription = 'chat';
const slashCommandPlaceholderDecorationType = 'chat-session-detail';
const slashCommandTextDecorationType = 'chat-session-text';
Expand Down Expand Up @@ -176,7 +176,7 @@ class InputEditorDecorations extends Disposable {
}

// const variables = this.chatVariablesService.getVariables();
const variableReg = /(^|\s)@(\w+)(?=(\s|$))/ig;
const variableReg = /(^|\s)@(\w+)(:\d+)?(?=(\s|$))/ig;
let match: RegExpMatchArray | null;
const varDecorations: IDecorationOptions[] = [];
while (match = variableReg.exec(inputValue)) {
Expand Down Expand Up @@ -309,17 +309,31 @@ class VariableCompletions extends Disposable {
replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn);
}

const history = widget.viewModel!.getItems()
.filter(isResponseVM);

// TODO@roblourens work out a real API for this- maybe it can be part of the two-step flow that @file will probably use
const historyItems = history.map((h, i): CompletionItem => ({
label: `@response:${i + 1}`,
detail: h.response.asString(),
insertText: `@response:${String(i + 1).padStart(String(history.length).length, '0')} `,
kind: CompletionItemKind.Text,
range: { insert, replace },
}));

const variableItems = Array.from(this.chatVariablesService.getVariables()).map(v => {
const withAt = `@${v.name}`;
return <CompletionItem>{
label: withAt,
range: { insert, replace },
insertText: withAt + ' ',
detail: v.description,
kind: CompletionItemKind.Text, // The icons are disabled here anyway,
};
});

return <CompletionList>{
suggestions: Array.from(this.chatVariablesService.getVariables()).map(v => {
const withAt = `@${v.name}`;
return <CompletionItem>{
label: withAt,
range: { insert, replace },
insertText: withAt + ' ',
detail: v.description,
kind: CompletionItemKind.Text, // The icons are disabled here anyway,
};
})
suggestions: [...variableItems, ...historyItems]
};
}
}));
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ export class ChatService extends Disposable implements IChatService {
};

if (typeof request.message === 'string') {
request.variables = await this.chatVariablesService.resolveVariables(request.message, token);
request.variables = await this.chatVariablesService.resolveVariables(request.message, model, token);
}

rawResponse = await provider.provideReply(request, progressCallback, token);
Expand Down
37 changes: 24 additions & 13 deletions src/vs/workbench/contrib/chat/common/chatVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';

export interface IChatVariableData {
name: string;
description: string;
hidden?: boolean;
canTakeArgument?: boolean;
}

export interface IChatRequestVariableValue {
Expand All @@ -22,7 +25,7 @@ export interface IChatRequestVariableValue {

export interface IChatVariableResolver {
// TODO should we spec "zoom level"
(messageText: string, token: CancellationToken): Promise<IChatRequestVariableValue[] | undefined>;
(messageText: string, arg: string | undefined, model: IChatModel, token: CancellationToken): Promise<IChatRequestVariableValue[] | undefined>;
}

export const IChatVariablesService = createDecorator<IChatVariablesService>('IChatVariablesService');
Expand All @@ -35,35 +38,42 @@ export interface IChatVariablesService {
/**
* Resolves all variables that occur in `prompt`
*/
resolveVariables(prompt: string, token: CancellationToken): Promise<Record<string, IChatRequestVariableValue[]>>;
resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise<Record<string, IChatRequestVariableValue[]>>;
}

type ChatData = [data: IChatVariableData, resolver: IChatVariableResolver];
interface IChatData {
data: IChatVariableData;
resolver: IChatVariableResolver;
}

export class ChatVariablesService implements IChatVariablesService {
declare _serviceBrand: undefined;

private _resolver = new Map<string, ChatData>();
private _resolver = new Map<string, IChatData>();

constructor() {
}

async resolveVariables(prompt: string, token: CancellationToken): Promise<Record<string, IChatRequestVariableValue[]>> {
async resolveVariables(prompt: string, model: IChatModel, token: CancellationToken): Promise<Record<string, IChatRequestVariableValue[]>> {
const resolvedVariables: Record<string, IChatRequestVariableValue[]> = {};
const jobs: Promise<any>[] = [];

const regex = /(^|\s)@(\w+)(\s|$)/ig;
const regex = /(^|\s)@(\w+)(:\w+)?(\s|$)/ig;

let match: RegExpMatchArray | null;
while (match = regex.exec(prompt)) {
const candidate = match[2];
const data = this._resolver.get(candidate.toLowerCase());
if (data) {
jobs.push(data[1](prompt, token).then(value => {
if (value) {
resolvedVariables[candidate] = value;
}
}).catch(onUnexpectedExternalError));
const arg = match[3];
if (!arg || data.data.canTakeArgument) {
const argWithoutColon = arg?.slice(1);
jobs.push(data.resolver(prompt, argWithoutColon, model, token).then(value => {
if (value) {
resolvedVariables[candidate + (arg ?? '')] = value;
}
}).catch(onUnexpectedExternalError));
}
}
}

Expand All @@ -73,15 +83,16 @@ export class ChatVariablesService implements IChatVariablesService {
}

getVariables(): Iterable<Readonly<IChatVariableData>> {
return Iterable.map(this._resolver.values(), data => data[0]);
const all = Iterable.map(this._resolver.values(), data => data.data);
return Iterable.filter(all, data => !data.hidden);
}

registerVariable(data: IChatVariableData, resolver: IChatVariableResolver): IDisposable {
const key = data.name.toLowerCase();
if (this._resolver.has(key)) {
throw new Error(`A chat variable with the name '${data.name}' already exists.`);
}
this._resolver.set(key, [data, resolver]);
this._resolver.set(key, { data, resolver });
return toDisposable(() => {
this._resolver.delete(key);
});
Expand Down
12 changes: 6 additions & 6 deletions src/vs/workbench/contrib/chat/test/common/chatVariables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,32 @@ suite('ChatVariables', function () {
service.registerVariable({ name: 'far', description: 'boo' }, async () => ([{ level: 'full', value: 'farboo' }]));

{
const data = await service.resolveVariables('Hello @foo and@far', CancellationToken.None);
const data = await service.resolveVariables('Hello @foo and@far', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 1);
assert.deepEqual(Object.keys(data).sort(), ['foo']);
}
{
const data = await service.resolveVariables('@foo Hello', CancellationToken.None);
const data = await service.resolveVariables('@foo Hello', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 1);
assert.deepEqual(Object.keys(data).sort(), ['foo']);
}
{
const data = await service.resolveVariables('Hello @foo', CancellationToken.None);
const data = await service.resolveVariables('Hello @foo', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 1);
assert.deepEqual(Object.keys(data).sort(), ['foo']);
}
{
const data = await service.resolveVariables('Hello @foo and@far @foo', CancellationToken.None);
const data = await service.resolveVariables('Hello @foo and@far @foo', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 1);
assert.deepEqual(Object.keys(data).sort(), ['foo']);
}
{
const data = await service.resolveVariables('Hello @foo and @far @foo', CancellationToken.None);
const data = await service.resolveVariables('Hello @foo and @far @foo', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 2);
assert.deepEqual(Object.keys(data).sort(), ['far', 'foo']);
}
{
const data = await service.resolveVariables('Hello @foo and @far @foo @unknown', CancellationToken.None);
const data = await service.resolveVariables('Hello @foo and @far @foo @unknown', null!, CancellationToken.None);
assert.strictEqual(Object.keys(data).length, 2);
assert.deepEqual(Object.keys(data).sort(), ['far', 'foo']);
}
Expand Down