diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotChatSessionsProvider.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotChatSessionsProvider.ts index e6ab1d7ad2a..7982d1c9ba8 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotChatSessionsProvider.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotChatSessionsProvider.ts @@ -6,14 +6,14 @@ import * as vscode from 'vscode'; import { ChatSessionItem } from 'vscode'; import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService'; -import { getGithubRepoIdFromFetchUrl, GithubRepoId, IGitService } from '../../../platform/git/common/gitService'; +import { IGitService } from '../../../platform/git/common/gitService'; import { PullRequestSearchItem, SessionInfo } from '../../../platform/github/common/githubAPI'; import { IOctoKitService, JobInfo, RemoteAgentJobPayload } from '../../../platform/github/common/githubService'; import { ILogService } from '../../../platform/log/common/logService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; import { Disposable } from '../../../util/vs/base/common/lifecycle'; import { UriHandlerPaths, UriHandlers } from '../vscode/chatSessionsUriHandler'; -import { body_suffix, CONTINUE_TRUNCATION, extractTitle, formatBodyPlaceholder, JOBS_API_VERSION, RemoteAgentResult, truncatePrompt } from '../vscode/copilotCodingAgentUtils'; +import { body_suffix, CONTINUE_TRUNCATION, extractTitle, formatBodyPlaceholder, getRepoId, JOBS_API_VERSION, RemoteAgentResult, truncatePrompt } from '../vscode/copilotCodingAgentUtils'; import { ChatSessionContentBuilder } from './copilotChatSessionContentBuilder'; type ConfirmationResult = { step: string; accepted: boolean; metadata?: CreatePromptMetadata /* | SomeOtherMetadata */ }; @@ -76,7 +76,7 @@ export class CopilotChatSessionsProvider extends Disposable implements vscode.Ch return this.chatSessionItemsPromise; } this.chatSessionItemsPromise = (async () => { - const repoId = await this.getRepoId(); + const repoId = await getRepoId(this._gitService); if (!repoId) { return []; } @@ -129,7 +129,7 @@ export class CopilotChatSessionsProvider extends Disposable implements vscode.Ch if (pr) { return pr; } - const repoId = await this.getRepoId(); + const repoId = await getRepoId(this._gitService); if (!repoId) { this.logService.warn('Failed to determine GitHub repo from workspace'); return undefined; @@ -164,22 +164,6 @@ export class CopilotChatSessionsProvider extends Disposable implements vscode.Ch return await vscode.env.asExternalUri(vscode.Uri.from({ scheme: vscode.env.uriScheme, authority: extensionId, path: UriHandlerPaths.External_OpenPullRequestWebview, query })); } - private async getRepoId(): Promise { - let timeout = 5000; - while (!this._gitService.isInitialized) { - await new Promise(resolve => setTimeout(resolve, 100)); - timeout -= 100; - if (timeout <= 0) { - break; - } - } - - const repo = this._gitService.activeRepository.get(); - if (repo && repo.remoteFetchUrls?.[0]) { - return getGithubRepoIdFromFetchUrl(repo.remoteFetchUrls[0]); - } - } - private async chatParticipantImpl(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { const startSession = async (source: string, prompt: string, history?: string, references?: readonly vscode.ChatPromptReference[]) => { /* __GDPR__ @@ -669,7 +653,7 @@ export class CopilotChatSessionsProvider extends Disposable implements vscode.Ch async invokeRemoteAgent(prompt: string, problemContext?: string, token?: vscode.CancellationToken, autoPushAndCommit = true, chatStream?: vscode.ChatResponseStream): Promise { // TODO: support selecting remote // await this.promptAndUpdatePreferredGitHubRemote(true); - const repoId = await this.getRepoId(); + const repoId = await getRepoId(this._gitService); if (!repoId) { return { error: vscode.l10n.t('Repository information is not available.'), state: 'error' }; } diff --git a/extensions/copilot/src/extension/chatSessions/vscode/chatSessionsUriHandler.ts b/extensions/copilot/src/extension/chatSessions/vscode/chatSessionsUriHandler.ts index ecc41b58239..02006dd2416 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode/chatSessionsUriHandler.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode/chatSessionsUriHandler.ts @@ -3,15 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { IGitService } from '../../../platform/git/common/gitService'; +import { IOctoKitService } from '../../../platform/github/common/githubService'; +import { encodeBase64, VSBuffer } from '../../../util/vs/base/common/buffer'; import { EXTENSION_ID } from '../../common/constants'; +import { getRepoId } from './copilotCodingAgentUtils'; const GHPR_EXTENSION_ID = 'GitHub.vscode-pull-request-github'; export enum UriHandlerPaths { - OpenSessionPullRequest = '/open-session-pull-request', + OpenSession = '/openAgentSession', External_OpenPullRequestWebview = '/open-pull-request-webview', } export const UriHandlers = { - [UriHandlerPaths.OpenSessionPullRequest]: EXTENSION_ID, + [UriHandlerPaths.OpenSession]: EXTENSION_ID, [UriHandlerPaths.External_OpenPullRequestWebview]: GHPR_EXTENSION_ID }; +export type CustomUriHandler = vscode.UriHandler & { canHandleUri(uri: vscode.Uri): boolean }; + +export class ChatSessionsUriHandler implements CustomUriHandler { + constructor( + @IOctoKitService private readonly _octoKitService: IOctoKitService, + @IGitService private readonly _gitService: IGitService, + ) { } + + async handleUri(uri: vscode.Uri): Promise { + switch (uri.path) { + case UriHandlerPaths.OpenSession: + { + const params = new URLSearchParams(uri.query); + const type = params.get('type'); + const prId = params.get('id'); + if (type?.startsWith('copilot') && prId) { + // For now we hardcode it to this type, eventually the full type should come in the URI + return this._openGitHubSession('copilot-cloud-agent', prId); + } + } + } + } + + private async _openGitHubSession(type: string, id: string): Promise { + const repoId = await getRepoId(this._gitService); + if (!repoId) { + return; + } + const pullRequests = await this._octoKitService.getCopilotPullRequestsForUser(repoId.org, repoId.repo); + const pullRequest = pullRequests.find(pr => pr.id === id); + if (!pullRequest) { + return; + } + const encodedId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(pullRequest.number.toString())), false, true); + const uri = vscode.Uri.from({ scheme: 'vscode-chat-session', authority: type, path: '/' + encodedId }); + await vscode.commands.executeCommand('vscode.open', uri); + } + + public canHandleUri(uri: vscode.Uri): boolean { + return Object.values(UriHandlerPaths).includes(uri.path as UriHandlerPaths); + } +} \ No newline at end of file diff --git a/extensions/copilot/src/extension/chatSessions/vscode/copilotCodingAgentUtils.ts b/extensions/copilot/src/extension/chatSessions/vscode/copilotCodingAgentUtils.ts index 180d41e07e8..83a79f3dffd 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode/copilotCodingAgentUtils.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode/copilotCodingAgentUtils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { getGithubRepoIdFromFetchUrl, GithubRepoId, IGitService } from '../../../platform/git/common/gitService'; import { ILogService } from '../../../platform/log/common/logService'; export const MAX_PROBLEM_STATEMENT_LENGTH = 30_000 - 50; // 50 character buffer @@ -68,4 +69,20 @@ export function extractTitle(prompt: string, context: string | undefined): strin export function formatBodyPlaceholder(title: string | undefined): string { return vscode.l10n.t('Coding agent has begun work on **{0}** and will update this pull request as work progresses.', title || vscode.l10n.t('your request')); +} + +export async function getRepoId(gitService: IGitService): Promise { + let timeout = 5000; + while (!gitService.isInitialized) { + await new Promise(resolve => setTimeout(resolve, 100)); + timeout -= 100; + if (timeout <= 0) { + break; + } + } + + const repo = gitService.activeRepository.get(); + if (repo && repo.remoteFetchUrls?.[0]) { + return getGithubRepoIdFromFetchUrl(repo.remoteFetchUrls[0]); + } } \ No newline at end of file diff --git a/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts b/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts index 1730a0cc50e..6cb9fbb82cd 100644 --- a/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts +++ b/extensions/copilot/src/extension/onboardDebug/vscode-node/copilotDebugCommandContribution.ts @@ -10,6 +10,8 @@ import * as vscode from 'vscode'; import { IAuthenticationService } from '../../../platform/authentication/common/authentication'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; +import { IGitService } from '../../../platform/git/common/gitService'; +import { IOctoKitService } from '../../../platform/github/common/githubService'; import { ILogService } from '../../../platform/log/common/logService'; import { ITasksService } from '../../../platform/tasks/common/tasksService'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry'; @@ -20,6 +22,7 @@ import { Disposable } from '../../../util/vs/base/common/lifecycle'; import * as path from '../../../util/vs/base/common/path'; import { URI } from '../../../util/vs/base/common/uri'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; +import { ChatSessionsUriHandler, CustomUriHandler } from '../../chatSessions/vscode/chatSessionsUriHandler'; import { EXTENSION_ID } from '../../common/constants'; import { ILaunchConfigService, needsWorkspaceFolderForTaskError } from '../common/launchConfigService'; import { CopilotDebugCommandSessionFactory } from '../node/copilotDebugCommandSessionFactory'; @@ -37,6 +40,7 @@ export const COPILOT_DEBUG_COMMAND = `copilot-debug`; const DEBUG_COMMAND_JS = 'copilotDebugCommand.js'; export class CopilotDebugCommandContribution extends Disposable implements vscode.UriHandler { + private chatSessionsUriHandler: CustomUriHandler; private registerSerializer: Promise; constructor( @@ -49,6 +53,8 @@ export class CopilotDebugCommandContribution extends Disposable implements vscod @ITelemetryService private readonly telemetryService: ITelemetryService, @ITasksService private readonly tasksService: ITasksService, @ITerminalService private readonly terminalService: ITerminalService, + @IOctoKitService private readonly _octoKitService: IOctoKitService, + @IGitService private readonly _gitService: IGitService ) { super(); @@ -65,6 +71,7 @@ export class CopilotDebugCommandContribution extends Disposable implements vscod })); this.registerSerializer = this.registerEnvironment(); + this.chatSessionsUriHandler = new ChatSessionsUriHandler(this._octoKitService, this._gitService); } private async ensureTask(workspaceFolder: URI | undefined, def: vscode.TaskDefinition, handle: CopilotDebugCommandHandle): Promise { @@ -93,6 +100,9 @@ export class CopilotDebugCommandContribution extends Disposable implements vscod } handleUri(uri: vscode.Uri): vscode.ProviderResult { + if (this.chatSessionsUriHandler.canHandleUri(uri)) { + return this.chatSessionsUriHandler.handleUri(uri); + } const pipePath = process.platform === 'win32' ? '\\\\.\\pipe\\' + uri.path.slice(1) : uri.path; const cts = new CancellationTokenSource(); diff --git a/extensions/copilot/src/platform/github/common/githubAPI.ts b/extensions/copilot/src/platform/github/common/githubAPI.ts index d9248861699..fed76314351 100644 --- a/extensions/copilot/src/platform/github/common/githubAPI.ts +++ b/extensions/copilot/src/platform/github/common/githubAPI.ts @@ -8,7 +8,7 @@ import { IFetcherService } from '../../networking/common/fetcherService'; import { ITelemetryService } from '../../telemetry/common/telemetry'; export interface PullRequestSearchItem { - id: number; + id: string; number: number; title: string; state: string;