Skip to content

Commit 64448b8

Browse files
committed
Add UpdateQueryTool for automatic query updates from chat
1 parent 96e8d4f commit 64448b8

File tree

5 files changed

+297
-3
lines changed

5 files changed

+297
-3
lines changed

src/common/settingsUtils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export function editQuery(namespace: string, queryName: string) {
9191
inputBox.dispose();
9292
} else if (inputBox.selectedItems[0] === inputBox.items[3]) {
9393
inputBox.ignoreFocusOut = true;
94-
await openCopilotForQuery(inputBox.value);
94+
await openCopilotForQuery(inputBox.value, namespace, queryName);
9595
inputBox.busy = false;
9696
}
9797
});
@@ -165,8 +165,8 @@ async function openSettingsAtQuery(config: vscode.WorkspaceConfiguration, inspec
165165
}
166166
}
167167

168-
async function openCopilotForQuery(currentQuery: string) {
169-
const chatMessage = vscode.l10n.t('I want to edit this GitHub search query: \n```\n{0}\n```\nOutput only one, minimally modified query in a codeblock.\nModify it so that it ', currentQuery);
168+
async function openCopilotForQuery(currentQuery: string, namespace: string, queryName: string) {
169+
const chatMessage = vscode.l10n.t('I want to edit this GitHub search query for "{0}": \n```\n{1}\n```\n\nPlease modify the query as needed and then use the update_query tool to save the changes automatically. The namespace is "{2}" and the query name is "{3}".', queryName, currentQuery, namespace, queryName);
170170

171171
// Open chat with the query pre-populated
172172
await vscode.commands.executeCommand(commands.OPEN_CHAT, { query: chatMessage, isPartialQuery: true });

src/lm/tools/tools.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import { ConvertToSearchSyntaxTool, SearchTool } from './searchTools';
2020
import { SuggestFixTool } from './suggestFixTool';
2121
import { IssueSummarizationTool } from './summarizeIssueTool';
2222
import { NotificationSummarizationTool } from './summarizeNotificationsTool';
23+
import { UpdateQueryTool } from './updateQueryTool';
2324

2425
export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState, copilotRemoteAgentManager: CopilotRemoteAgentManager, telemetry: ITelemetry) {
2526
registerFetchingTools(context, credentialStore, repositoriesManager, chatParticipantState);
2627
registerSummarizationTools(context);
2728
registerSuggestFixTool(context, credentialStore, repositoriesManager, chatParticipantState);
2829
registerSearchTools(context, credentialStore, repositoriesManager, chatParticipantState);
2930
registerCopilotAgentTools(context, copilotRemoteAgentManager, telemetry);
31+
registerUpdateQueryTool(context);
3032
context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager, copilotRemoteAgentManager)));
3133
context.subscriptions.push(vscode.lm.registerTool(OpenPullRequestTool.toolId, new OpenPullRequestTool(repositoriesManager, copilotRemoteAgentManager)));
3234
}
@@ -53,4 +55,8 @@ function registerSearchTools(context: vscode.ExtensionContext, credentialStore:
5355
context.subscriptions.push(vscode.lm.registerTool(ConvertToSearchSyntaxTool.toolId, new ConvertToSearchSyntaxTool(credentialStore, repositoriesManager, chatParticipantState)));
5456
context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager, chatParticipantState)));
5557
context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(context, chatParticipantState)));
58+
}
59+
60+
function registerUpdateQueryTool(context: vscode.ExtensionContext) {
61+
context.subscriptions.push(vscode.lm.registerTool(UpdateQueryTool.toolId, new UpdateQueryTool()));
5662
}

src/lm/tools/updateQueryTool.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import * as vscode from 'vscode';
8+
import { ISSUES_SETTINGS_NAMESPACE, PR_SETTINGS_NAMESPACE, QUERIES } from '../../common/settingKeys';
9+
10+
interface UpdateQueryParameters {
11+
namespace: string;
12+
queryName: string;
13+
newQuery: string;
14+
}
15+
16+
export class UpdateQueryTool implements vscode.LanguageModelTool<UpdateQueryParameters> {
17+
public static readonly toolId = 'github-pull-request_update_query';
18+
19+
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<UpdateQueryParameters>): Promise<vscode.PreparedToolInvocation> {
20+
return {
21+
invocationMessage: vscode.l10n.t('Updating query "{0}"', options.input.queryName || 'unnamed')
22+
};
23+
}
24+
25+
async invoke(options: vscode.LanguageModelToolInvocationOptions<UpdateQueryParameters>, _token: vscode.CancellationToken): Promise<vscode.LanguageModelToolResult | undefined> {
26+
const { namespace, queryName, newQuery } = options.input;
27+
28+
// Validate inputs
29+
if (!namespace || !queryName || !newQuery) {
30+
const errorMessage = vscode.l10n.t('Missing required parameters: namespace, queryName, and newQuery are all required');
31+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(errorMessage)]);
32+
}
33+
34+
// Validate namespace
35+
if (namespace !== PR_SETTINGS_NAMESPACE && namespace !== ISSUES_SETTINGS_NAMESPACE) {
36+
const errorMessage = vscode.l10n.t('Invalid namespace: must be either "{0}" or "{1}"', PR_SETTINGS_NAMESPACE, ISSUES_SETTINGS_NAMESPACE);
37+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(errorMessage)]);
38+
}
39+
40+
try {
41+
const config = vscode.workspace.getConfiguration(namespace);
42+
const inspect = config.inspect<{ label: string; query: string }[]>(QUERIES);
43+
44+
// Find the target to update based on the hierarchy
45+
let newValue: { label: string; query: string }[];
46+
let target: vscode.ConfigurationTarget;
47+
48+
if (inspect?.workspaceFolderValue) {
49+
target = vscode.ConfigurationTarget.WorkspaceFolder;
50+
newValue = [...inspect.workspaceFolderValue];
51+
} else if (inspect?.workspaceValue) {
52+
target = vscode.ConfigurationTarget.Workspace;
53+
newValue = [...inspect.workspaceValue];
54+
} else {
55+
target = vscode.ConfigurationTarget.Global;
56+
newValue = [...(config.get<{ label: string; query: string }[]>(QUERIES) ?? [])];
57+
}
58+
59+
// Find and update the query
60+
const queryIndex = newValue.findIndex(query => query.label === queryName);
61+
if (queryIndex === -1) {
62+
const errorMessage = vscode.l10n.t('Query "{0}" not found', queryName);
63+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(errorMessage)]);
64+
}
65+
66+
const oldQuery = newValue[queryIndex].query;
67+
newValue[queryIndex].query = newQuery;
68+
69+
// Update the configuration
70+
await config.update(QUERIES, newValue, target);
71+
72+
const successMessage = vscode.l10n.t('Successfully updated query "{0}" from "{1}" to "{2}"', queryName, oldQuery, newQuery);
73+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(successMessage)]);
74+
75+
} catch (error) {
76+
const errorMessage = vscode.l10n.t('Failed to update query: {0}', error instanceof Error ? error.message : String(error));
77+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(errorMessage)]);
78+
}
79+
}
80+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
'use strict';
6+
7+
import * as vscode from 'vscode';
8+
import { FetchIssueResult } from './fetchIssueTool';
9+
import { concatAsyncIterable } from './toolsUtils';
10+
11+
export class IssueSummarizationTool implements vscode.LanguageModelTool<FetchIssueResult> {
12+
public static readonly toolId = 'github-pull-request_issue_summarize';
13+
14+
async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<FetchIssueResult>): Promise<vscode.PreparedToolInvocation> {
15+
if (!options.input.title) {
16+
return {
17+
invocationMessage: vscode.l10n.t('Summarizing issue')
18+
};
19+
}
20+
const shortenedTitle = options.input.title.length > 40;
21+
const maxLengthTitle = shortenedTitle ? options.input.title.substring(0, 40) : options.input.title;
22+
return {
23+
invocationMessage: vscode.l10n.t('Summarizing "{0}', maxLengthTitle)
24+
};
25+
}
26+
27+
async invoke(options: vscode.LanguageModelToolInvocationOptions<FetchIssueResult>, _token: vscode.CancellationToken): Promise<vscode.LanguageModelToolResult | undefined> {
28+
let issueOrPullRequestInfo: string = `
29+
Title : ${options.input.title}
30+
Body : ${options.input.body}
31+
`;
32+
const fileChanges = options.input.fileChanges;
33+
if (fileChanges) {
34+
issueOrPullRequestInfo += `
35+
The following are the files changed:
36+
`;
37+
for (const fileChange of fileChanges.values()) {
38+
issueOrPullRequestInfo += `
39+
File : ${fileChange.fileName}
40+
Patch: ${fileChange.patch}
41+
`;
42+
}
43+
}
44+
const comments = options.input.comments;
45+
if (comments) {
46+
for (const [index, comment] of comments.entries()) {
47+
issueOrPullRequestInfo += `
48+
Comment ${index} :
49+
Author: ${comment.author}
50+
Body: ${comment.body}
51+
`;
52+
}
53+
}
54+
const models = await vscode.lm.selectChatModels({
55+
vendor: 'copilot',
56+
family: 'gpt-4o'
57+
});
58+
const model = models[0];
59+
const repo = options.input.repo;
60+
const owner = options.input.owner;
61+
62+
if (model && repo && owner) {
63+
const messages = [vscode.LanguageModelChatMessage.User(this.summarizeInstructions(repo, owner))];
64+
messages.push(vscode.LanguageModelChatMessage.User(`The issue or pull request information is as follows:`));
65+
messages.push(vscode.LanguageModelChatMessage.User(issueOrPullRequestInfo));
66+
const response = await model.sendRequest(messages, {});
67+
const responseText = await concatAsyncIterable(response.text);
68+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(responseText)]);
69+
} else {
70+
return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(issueOrPullRequestInfo)]);
71+
}
72+
}
73+
74+
private summarizeInstructions(repo: string, owner: string): string {
75+
return `
76+
You are an AI assistant who is very proficient in summarizing issues and pull requests (PRs).
77+
You will be given information relative to an issue or PR : the title, the body and the comments. In the case of a PR you will also be given patches of the PR changes.
78+
Your task is to output a summary of all this information.
79+
Do not output code. When you try to summarize PR changes, summarize in a textual format.
80+
Output references to other issues and PRs as Markdown links. The current issue has owner ${owner} and is in the repo ${repo}.
81+
If a comment references for example issue or PR #123, then output either of the following in the summary depending on if it is an issue or a PR:
82+
83+
[#123](https://github.com/${owner}/${repo}/issues/123)
84+
[#123](https://github.com/${owner}/${repo}/pull/123)
85+
86+
When you summarize comments, always give a summary of each comment and always mention the author clearly before the comment. If the author is called 'joe' and the comment is 'this is a comment', then the output should be:
87+
88+
joe: this is a comment
89+
90+
Make sure the summary is at least as short or shorter than the issue or PR with the comments and the patches if there are.
91+
`;
92+
}
93+
94+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { default as assert } from 'assert';
7+
import { SinonSandbox, createSandbox } from 'sinon';
8+
import * as vscode from 'vscode';
9+
import { UpdateQueryTool } from '../../../lm/tools/updateQueryTool';
10+
import { ISSUES_SETTINGS_NAMESPACE, PR_SETTINGS_NAMESPACE } from '../../../common/settingKeys';
11+
12+
describe('UpdateQueryTool', function () {
13+
let sinon: SinonSandbox;
14+
let tool: UpdateQueryTool;
15+
16+
beforeEach(function () {
17+
sinon = createSandbox();
18+
tool = new UpdateQueryTool();
19+
});
20+
21+
afterEach(function () {
22+
sinon.restore();
23+
});
24+
25+
describe('toolId', function () {
26+
it('should have the correct tool ID', function () {
27+
assert.strictEqual(UpdateQueryTool.toolId, 'github-pull-request_update_query');
28+
});
29+
});
30+
31+
describe('prepareInvocation()', function () {
32+
it('should return the correct invocation message', async function () {
33+
const mockInput = {
34+
namespace: PR_SETTINGS_NAMESPACE,
35+
queryName: 'My Queries',
36+
newQuery: 'is:pr state:open'
37+
};
38+
39+
const result = await tool.prepareInvocation({ input: mockInput } as any);
40+
assert.strictEqual(result.invocationMessage, 'Updating query "My Queries"');
41+
});
42+
43+
it('should handle unnamed query', async function () {
44+
const mockInput = {
45+
namespace: PR_SETTINGS_NAMESPACE,
46+
queryName: '',
47+
newQuery: 'is:pr state:open'
48+
};
49+
50+
const result = await tool.prepareInvocation({ input: mockInput } as any);
51+
assert.strictEqual(result.invocationMessage, 'Updating query "unnamed"');
52+
});
53+
});
54+
55+
describe('invoke()', function () {
56+
it('should return error for missing parameters', async function () {
57+
const mockInput = {
58+
namespace: '',
59+
queryName: '',
60+
newQuery: ''
61+
};
62+
63+
const result = await tool.invoke({ input: mockInput } as any, {} as any);
64+
assert.ok(result);
65+
const textPart = result.content[0] as vscode.LanguageModelTextPart;
66+
assert.ok(textPart.value.includes('Missing required parameters'));
67+
});
68+
69+
it('should return error for invalid namespace', async function () {
70+
const mockInput = {
71+
namespace: 'invalid',
72+
queryName: 'test',
73+
newQuery: 'is:pr state:open'
74+
};
75+
76+
const result = await tool.invoke({ input: mockInput } as any, {} as any);
77+
assert.ok(result);
78+
const textPart = result.content[0] as vscode.LanguageModelTextPart;
79+
assert.ok(textPart.value.includes('Invalid namespace'));
80+
});
81+
82+
it('should accept valid namespaces', async function () {
83+
// Mock workspace configuration
84+
const mockConfig = {
85+
inspect: sinon.stub().returns({ workspaceValue: [{ label: 'test', query: 'old query' }] }),
86+
get: sinon.stub().returns([{ label: 'test', query: 'old query' }]),
87+
update: sinon.stub().resolves()
88+
};
89+
sinon.stub(vscode.workspace, 'getConfiguration').returns(mockConfig as any);
90+
91+
const prInput = {
92+
namespace: PR_SETTINGS_NAMESPACE,
93+
queryName: 'test',
94+
newQuery: 'is:pr state:open'
95+
};
96+
97+
const result1 = await tool.invoke({ input: prInput } as any, {} as any);
98+
assert.ok(result1);
99+
const textPart1 = result1.content[0] as vscode.LanguageModelTextPart;
100+
assert.ok(textPart1.value.includes('Successfully updated query'));
101+
102+
const issuesInput = {
103+
namespace: ISSUES_SETTINGS_NAMESPACE,
104+
queryName: 'test',
105+
newQuery: 'is:issue state:open'
106+
};
107+
108+
const result2 = await tool.invoke({ input: issuesInput } as any, {} as any);
109+
assert.ok(result2);
110+
const textPart2 = result2.content[0] as vscode.LanguageModelTextPart;
111+
assert.ok(textPart2.value.includes('Successfully updated query'));
112+
});
113+
});
114+
});

0 commit comments

Comments
 (0)