Skip to content

Commit 8e8d2f4

Browse files
authored
Implement PR and issue chat context providers (#8169)
Part of microsoft/vscode#271104
1 parent f16f0e4 commit 8e8d2f4

16 files changed

+310
-55
lines changed

package.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"enabledApiProposals": [
1414
"activeComment",
15+
"chatContextProvider",
1516
"chatParticipantAdditions",
1617
"chatParticipantPrivate",
1718
"chatSessionsProvider@3",
@@ -42,7 +43,7 @@
4243
"version": "0.122.0",
4344
"publisher": "GitHub",
4445
"engines": {
45-
"vscode": "^1.106.0"
46+
"vscode": "^1.107.0"
4647
},
4748
"categories": [
4849
"Other",
@@ -60,7 +61,9 @@
6061
"onFileSystem:githubcommit",
6162
"onFileSystem:review",
6263
"onWebviewPanel:IssueOverview",
63-
"onWebviewPanel:PullRequestOverview"
64+
"onWebviewPanel:PullRequestOverview",
65+
"onChatContextProvider:githubpr",
66+
"onChatContextProvider:githubissue"
6467
],
6568
"browser": "./dist/browser/extension",
6669
"l10n": "./dist/browser/extension",
@@ -72,6 +75,18 @@
7275
"virtualWorkspaces": true
7376
},
7477
"contributes": {
78+
"chatContext": [
79+
{
80+
"id": "githubpr",
81+
"icon": "$(github)",
82+
"displayName": "GitHub Pull Requests"
83+
},
84+
{
85+
"id": "githubissue",
86+
"icon": "$(issues)",
87+
"displayName": "GitHub Issues"
88+
}
89+
],
7590
"chatSessions": [
7691
{
7792
"type": "copilot-swe-agent",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
7+
declare module 'vscode' {
8+
9+
// https://github.com/microsoft/vscode/issues/271104 @alexr00
10+
11+
export namespace chat {
12+
13+
// TODO@alexr00 API:
14+
// selector is confusing
15+
export function registerChatContextProvider(selector: DocumentSelector, id: string, provider: ChatContextProvider): Disposable;
16+
17+
}
18+
19+
export interface ChatContextItem {
20+
icon: ThemeIcon;
21+
label: string;
22+
modelDescription?: string;
23+
value?: string;
24+
}
25+
26+
export interface ChatContextProvider<T extends ChatContextItem = ChatContextItem> {
27+
28+
/**
29+
* Provide a list of chat context items that a user can choose from. These context items are shown as options when the user explicitly attaches context.
30+
* Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`.
31+
* `resolveChatContext` is only called for items that do not have a `value`.
32+
*
33+
* @param options
34+
* @param token
35+
*/
36+
provideChatContextExplicit?(token: CancellationToken): ProviderResult<T[]>;
37+
38+
/**
39+
* Given a particular resource, provide a chat context item for it. This is used for implicit context (see the settings `chat.implicitContext.enabled` and `chat.implicitContext.suggestedContext`).
40+
* Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`.
41+
* `resolveChatContext` is only called for items that do not have a `value`.
42+
*
43+
* @param resource
44+
* @param options
45+
* @param token
46+
*/
47+
provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult<T | undefined>;
48+
49+
/**
50+
* If a chat context item is provided without a `value`, from either of the `provide` methods, this method is called to resolve the `value` for the item.
51+
*
52+
* @param context
53+
* @param token
54+
*/
55+
resolveChatContext(context: T, token: CancellationToken): ProviderResult<ChatContextItem>;
56+
}
57+
58+
}

src/extension.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitP
3232
import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider';
3333
import { GitLensIntegration } from './integrations/gitlens/gitlensImpl';
3434
import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar';
35+
import { StateManager } from './issues/stateManager';
36+
import { IssueContextProvider } from './lm/issueContextProvider';
3537
import { ChatParticipant, ChatParticipantState } from './lm/participants';
38+
import { PullRequestContextProvider } from './lm/pullRequestContextProvider';
3639
import { registerTools } from './lm/tools/tools';
3740
import { migrate } from './migrations';
3841
import { NotificationsFeatureRegister } from './notifications/notificationsFeatureRegistar';
@@ -253,10 +256,14 @@ async function init(
253256
const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
254257
await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat');
255258

256-
const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry, copilotRemoteAgentManager);
259+
const issueStateManager = new StateManager(git, reposManager, context);
260+
const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry, issueStateManager, copilotRemoteAgentManager);
257261
context.subscriptions.push(issuesFeatures);
258262
await issuesFeatures.initialize();
259263

264+
vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', new PullRequestContextProvider(prsTreeModel, reposManager));
265+
vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-IssueOverview**' }, 'githubissue', new IssueContextProvider(issueStateManager, reposManager));
266+
260267
const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry, notificationsManager);
261268
context.subscriptions.push(notificationsFeatures);
262269

src/github/copilotPrWatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export class CopilotPRWatcher extends Disposable {
268268
private async _updateSingleState(pr: PullRequestModel): Promise<void> {
269269
const changes: CodingAgentPRAndStatus[] = [];
270270

271-
const copilotEvents = await pr.getCopilotTimelineEvents(pr, false, !this._model.isInitialized);
271+
const copilotEvents = await pr.getCopilotTimelineEvents(false, !this._model.isInitialized);
272272
let latestEvent = copilotEventToStatus(copilotEvents[copilotEvents.length - 1]);
273273
if (latestEvent === CopilotPRStatus.None) {
274274
if (!COPILOT_ACCOUNTS[pr.author.login]) {

src/github/folderRepositoryManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2499,7 +2499,7 @@ export class FolderRepositoryManager extends Disposable {
24992499
private async promptPullBrach(pr: PullRequestModel, branch: Branch, autoStashSetting?: boolean) {
25002500
if (!this._updateMessageShown || autoStashSetting) {
25012501
// When the PR is from Copilot, we only want to show the notification when Copilot is done working
2502-
const copilotStatus = await pr.copilotWorkingStatus(pr);
2502+
const copilotStatus = await pr.copilotWorkingStatus();
25032503
if (copilotStatus === CopilotWorkingStatus.InProgress) {
25042504
return;
25052505
}

src/github/issueModel.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
8585
}
8686
}
8787

88-
get timelineEvents(): readonly TimelineEvent[] {
89-
return this._timelineEvents ?? [];
88+
get timelineEvents(): readonly TimelineEvent[] | undefined {
89+
return this._timelineEvents;
9090
}
9191

9292
protected set timelineEvents(timelineEvents: readonly TimelineEvent[]) {
@@ -469,8 +469,8 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
469469
}
470470
}
471471

472-
async getIssueTimelineEvents(issueModel: IssueModel): Promise<TimelineEvent[]> {
473-
Logger.debug(`Fetch timeline events of issue #${issueModel.number} - enter`, GitHubRepository.ID);
472+
async getIssueTimelineEvents(): Promise<TimelineEvent[]> {
473+
Logger.debug(`Fetch timeline events of issue #${this.number} - enter`, GitHubRepository.ID);
474474
const { query, remote, schema } = await this.githubRepository.ensure();
475475

476476
try {
@@ -479,7 +479,7 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
479479
variables: {
480480
owner: remote.owner,
481481
name: remote.repositoryName,
482-
number: issueModel.number,
482+
number: this.number,
483483
},
484484
});
485485

@@ -489,11 +489,11 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
489489
}
490490

491491
const ret = data.repository.pullRequest.timelineItems.nodes;
492-
const events = await parseCombinedTimelineEvents(ret, await this.getCopilotTimelineEvents(issueModel, true), this.githubRepository);
492+
const events = await parseCombinedTimelineEvents(ret, await this.getCopilotTimelineEvents(true), this.githubRepository);
493493

494494
const crossRefs = events.filter((event): event is CrossReferencedEvent => {
495495
if ((event.event === EventType.CrossReferenced) && !event.source.isIssue) {
496-
return !this.githubRepository.getExistingPullRequestModel(event.source.number) && (compareIgnoreCase(event.source.owner, issueModel.remote.owner) === 0 && compareIgnoreCase(event.source.repo, issueModel.remote.repositoryName) === 0);
496+
return !this.githubRepository.getExistingPullRequestModel(event.source.number) && (compareIgnoreCase(event.source.owner, this.remote.owner) === 0 && compareIgnoreCase(event.source.repo, this.remote.repositoryName) === 0);
497497
}
498498
return false;
499499

@@ -504,7 +504,7 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
504504
this.githubRepository.getPullRequest(unseenPrs.source.number);
505505
}
506506

507-
issueModel.timelineEvents = events;
507+
this.timelineEvents = events;
508508
return events;
509509
} catch (e) {
510510
console.log(e);
@@ -515,55 +515,55 @@ export class IssueModel<TItem extends Issue = Issue> extends Disposable {
515515
/**
516516
* TODO: @alexr00 we should delete this https://github.com/microsoft/vscode-pull-request-github/issues/6965
517517
*/
518-
async getCopilotTimelineEvents(issueModel: IssueModel, skipMerge: boolean = false, useCache: boolean = false): Promise<TimelineEvent[]> {
519-
if (!COPILOT_ACCOUNTS[issueModel.author.login]) {
518+
async getCopilotTimelineEvents(skipMerge: boolean = false, useCache: boolean = false): Promise<TimelineEvent[]> {
519+
if (!COPILOT_ACCOUNTS[this.author.login]) {
520520
return [];
521521
}
522522

523-
Logger.debug(`Fetch Copilot timeline events of issue #${issueModel.number} - enter`, GitHubRepository.ID);
523+
Logger.debug(`Fetch Copilot timeline events of issue #${this.number} - enter`, GitHubRepository.ID);
524524

525525
if (useCache && this._copilotTimelineEvents) {
526-
Logger.debug(`Fetch Copilot timeline events of issue #${issueModel.number} (used cache) - exit`, GitHubRepository.ID);
526+
Logger.debug(`Fetch Copilot timeline events of issue #${this.number} (used cache) - exit`, GitHubRepository.ID);
527527

528528
return this._copilotTimelineEvents;
529529
}
530530

531531
const { octokit, remote } = await this.githubRepository.ensure();
532532
try {
533533
const timeline = await restPaginate<typeof octokit.api.issues.listEventsForTimeline, OctokitCommon.ListEventsForTimelineResponse>(octokit.api.issues.listEventsForTimeline, {
534-
issue_number: issueModel.number,
534+
issue_number: this.number,
535535
owner: remote.owner,
536536
repo: remote.repositoryName,
537537
per_page: 100
538538
});
539539

540-
const timelineEvents = parseSelectRestTimelineEvents(issueModel, timeline);
540+
const timelineEvents = parseSelectRestTimelineEvents(this, timeline);
541541
this._copilotTimelineEvents = timelineEvents;
542542
if (timelineEvents.length === 0) {
543543
return [];
544544
}
545545
if (!skipMerge) {
546-
const oldLastEvent = issueModel.timelineEvents.length > 0 ? issueModel.timelineEvents[issueModel.timelineEvents.length - 1] : undefined;
546+
const oldLastEvent = this.timelineEvents ? (this.timelineEvents.length > 0 ? this.timelineEvents[this.timelineEvents.length - 1] : undefined) : undefined;
547547
let allEvents: TimelineEvent[];
548548
if (!oldLastEvent) {
549549
allEvents = timelineEvents;
550550
} else {
551551
const oldEventTime = (eventTime(oldLastEvent) ?? 0);
552552
const newEvents = timelineEvents.filter(event => (eventTime(event) ?? 0) > oldEventTime);
553-
allEvents = [...issueModel.timelineEvents, ...newEvents];
553+
allEvents = [...(this.timelineEvents ?? []), ...newEvents];
554554
}
555-
issueModel.timelineEvents = allEvents;
555+
this.timelineEvents = allEvents;
556556
}
557-
Logger.debug(`Fetch Copilot timeline events of issue #${issueModel.number} - exit`, GitHubRepository.ID);
557+
Logger.debug(`Fetch Copilot timeline events of issue #${this.number} - exit`, GitHubRepository.ID);
558558
return timelineEvents;
559559
} catch (e) {
560-
Logger.error(`Error fetching Copilot timeline events of issue #${issueModel.number} - ${formatError(e)}`, GitHubRepository.ID);
560+
Logger.error(`Error fetching Copilot timeline events of issue #${this.number} - ${formatError(e)}`, GitHubRepository.ID);
561561
return [];
562562
}
563563
}
564564

565-
async copilotWorkingStatus(issueModel: IssueModel): Promise<CopilotWorkingStatus | undefined> {
566-
const copilotEvents = await this.getCopilotTimelineEvents(issueModel);
565+
async copilotWorkingStatus(): Promise<CopilotWorkingStatus | undefined> {
566+
const copilotEvents = await this.getCopilotTimelineEvents();
567567
if (copilotEvents.length > 0) {
568568
const lastEvent = copilotEvents[copilotEvents.length - 1];
569569
if (lastEvent.event === EventType.CopilotFinished) {

src/github/issueOverview.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
255255
issueModel.remote.repositoryName,
256256
issueModel.number,
257257
),
258-
issueModel.getIssueTimelineEvents(issueModel),
258+
issueModel.getIssueTimelineEvents(),
259259
this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(issueModel),
260260
issueModel.canEdit(),
261261
this._folderRepositoryManager.getAssignableUsers(),
@@ -509,7 +509,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
509509
}
510510

511511
protected _getTimeline(): Promise<TimelineEvent[]> {
512-
return this._item.getIssueTimelineEvents(this._item);
512+
return this._item.getIssueTimelineEvents();
513513
}
514514

515515
private async changeAssignees(message: IRequestMessage<void>): Promise<void> {

0 commit comments

Comments
 (0)