diff --git a/src/@types/vscode.proposed.chatContextProvider.d.ts b/src/@types/vscode.proposed.chatContextProvider.d.ts index 38ea26573a..173c5dd11f 100644 --- a/src/@types/vscode.proposed.chatContextProvider.d.ts +++ b/src/@types/vscode.proposed.chatContextProvider.d.ts @@ -10,28 +10,61 @@ declare module 'vscode' { export namespace chat { - // TODO@alexr00 API: - // selector is confusing - export function registerChatContextProvider(selector: DocumentSelector, id: string, provider: ChatContextProvider): Disposable; + /** + * Register a chat context provider. Chat context can be provided: + * - For a resource. Make sure to pass a selector that matches the resource you want to provide context for. + * Providers registered without a selector will not be called for resource-based context. + * - Explicitly. These context items are shown as options when the user explicitly attaches context. + * + * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * + * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. + * @param id Unique identifier for the provider. + * @param provider The chat context provider. + */ + export function registerChatContextProvider(selector: DocumentSelector | undefined, id: string, provider: ChatContextProvider): Disposable; } export interface ChatContextItem { + /** + * Icon for the context item. + */ icon: ThemeIcon; + /** + * Human readable label for the context item. + */ label: string; + /** + * An optional description of the context item, e.g. to describe the item to the language model. + */ modelDescription?: string; + /** + * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. + */ value?: string; } export interface ChatContextProvider { + /** + * An optional event that should be fired when the workspace chat context has changed. + */ + onDidChangeWorkspaceChatContext?: Event; + + /** + * Provide a list of chat context items to be included as workspace context for all chat sessions. + * + * @param token A cancellation token. + */ + provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + /** * 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. * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param options - * @param token + * @param token A cancellation token. */ provideChatContextExplicit?(token: CancellationToken): ProviderResult; @@ -40,17 +73,16 @@ declare module 'vscode' { * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. * `resolveChatContext` is only called for items that do not have a `value`. * - * @param resource - * @param options - * @param token + * @param options Options include the resource for which to provide context. + * @param token A cancellation token. */ provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; /** * 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. * - * @param context - * @param token + * @param context The context item to resolve. + * @param token A cancellation token. */ resolveChatContext(context: T, token: CancellationToken): ProviderResult; } diff --git a/src/extension.ts b/src/extension.ts index daaf4297b8..8c95741854 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -261,8 +261,10 @@ async function init( context.subscriptions.push(issuesFeatures); await issuesFeatures.initialize(); - vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', new PullRequestContextProvider(prsTreeModel, reposManager)); + const pullRequestContextProvider = new PullRequestContextProvider(prsTreeModel, reposManager, git); + vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', pullRequestContextProvider); vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-IssueOverview**' }, 'githubissue', new IssueContextProvider(issueStateManager, reposManager)); + pullRequestContextProvider.initialize(); const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry, notificationsManager); context.subscriptions.push(notificationsFeatures); diff --git a/src/lm/pullRequestContextProvider.ts b/src/lm/pullRequestContextProvider.ts index 31203b236e..ec18aa6416 100644 --- a/src/lm/pullRequestContextProvider.ts +++ b/src/lm/pullRequestContextProvider.ts @@ -4,19 +4,84 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { GitApiImpl } from '../api/api1'; +import { Disposable } from '../common/lifecycle'; +import { onceEvent } from '../common/utils'; import { PullRequestModel } from '../github/pullRequestModel'; import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; import { RepositoriesManager } from '../github/repositoriesManager'; import { PrsTreeModel } from '../view/prsTreeModel'; interface PRChatContextItem extends vscode.ChatContextItem { - pr: PullRequestModel; + pr?: PullRequestModel; } -export class PullRequestContextProvider implements vscode.ChatContextProvider { +export class PullRequestContextProvider extends Disposable implements vscode.ChatContextProvider { + private readonly _onDidChangeWorkspaceChatContext = new vscode.EventEmitter(); + readonly onDidChangeWorkspaceChatContext = this._onDidChangeWorkspaceChatContext.event; + constructor(private readonly _prsTreeModel: PrsTreeModel, - private readonly _reposManager: RepositoriesManager - ) { } + private readonly _reposManager: RepositoriesManager, + private readonly _git: GitApiImpl + ) { + super(); + } + + /** + * Do this setup in the initialize method so that it can be called after the provider is registered. + */ + async initialize() { + if (this._git.state === 'uninitialized') { + await new Promise(resolve => { + this._register(onceEvent(this._git.onDidChangeState)(() => resolve())); + }); + } + this._reposManager.folderManagers.forEach(folderManager => { + this._register(folderManager.onDidChangeActivePullRequest(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + }); + this._register(this._reposManager.onDidChangeFolderRepositories(e => { + if (!e.added) { + return; + } + this._register(e.added.onDidChangeActivePullRequest(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._register(this._reposManager.onDidChangeAnyGitHubRepository(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._onDidChangeWorkspaceChatContext.fire(); + } + + async provideWorkspaceChatContext(_token: vscode.CancellationToken): Promise { + const modelDescription = this._reposManager.folderManagers.length > 1 ? 'Information about one of the current repositories. You can use this information when you need to calculate diffs or compare changes with the default branch' : 'Information about the current repository. You can use this information when you need to calculate diffs or compare changes with the default branch'; + const contexts: vscode.ChatContextItem[] = []; + for (const folderManager of this._reposManager.folderManagers) { + if (folderManager.gitHubRepositories.length === 0) { + continue; + } + const defaults = await folderManager.getPullRequestDefaults(); + + let value = `Repository name: ${defaults.repo} +Owner: ${defaults.owner} +Current branch: ${folderManager.repository.state.HEAD?.name ?? 'unknown'} +Default branch: ${defaults.base}`; + if (folderManager.activePullRequest) { + value = `${value} +Active pull request (may not be the same as open pull request): #${folderManager.activePullRequest.number} ${folderManager.activePullRequest.html_url}`; + } + contexts.push({ + icon: new vscode.ThemeIcon('github-alt'), + label: `${defaults.owner}/${defaults.repo}`, + modelDescription, + value + }); + } + return contexts; + } async provideChatContextForResource(_options: { resource: vscode.Uri }, _token: vscode.CancellationToken): Promise { const item = PullRequestOverviewPanel.currentPanel?.getCurrentItem(); @@ -26,6 +91,9 @@ export class PullRequestContextProvider implements vscode.ChatContextProvider { } async resolveChatContext(context: PRChatContextItem, _token: vscode.CancellationToken): Promise { + if (!context.pr) { + return context; + } context.value = await this._resolvedPrValue(context.pr); context.modelDescription = 'All the information about the GitHub pull request the user is viewing, including comments, review threads, and changes.'; return context;