Chat sessions URI handler (#1380)

* Chat sessions URI handler

* Type change

* Update src/extension/chatSessions/vscode/chatSessionsUriHandler.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/extension/chatSessions/vscode/chatSessionsUriHandler.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Osvaldo Ortega
2025-10-16 22:13:54 +00:00
committed by GitHub
co-authored by Copilot
parent d41e63e2b0
commit 106ee0e628
5 changed files with 82 additions and 24 deletions
@@ -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<GithubRepoId | undefined> {
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<RemoteAgentResult> {
// 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' };
}
@@ -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<void> {
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<void> {
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);
}
}
@@ -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<GithubRepoId | undefined> {
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]);
}
}
@@ -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<void>;
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<boolean> {
@@ -93,6 +100,9 @@ export class CopilotDebugCommandContribution extends Disposable implements vscod
}
handleUri(uri: vscode.Uri): vscode.ProviderResult<void> {
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();
@@ -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;