From b44c1ed8d482cf39e7e6cedcce2334dc6406097a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:31:21 +0000 Subject: [PATCH 01/12] Initial plan From cf080272872015150b899c6853642c4c14575fc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:33:58 +0000 Subject: [PATCH 02/12] initial plan for checkout PR in worktree feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- .../vscode.proposed.chatContextProvider.d.ts | 2 - ...ode.proposed.chatParticipantAdditions.d.ts | 231 +++++++++++++++++- ...scode.proposed.chatParticipantPrivate.d.ts | 60 ++++- .../vscode.proposed.chatSessionsProvider.d.ts | 43 ++-- 4 files changed, 297 insertions(+), 39 deletions(-) diff --git a/src/@types/vscode.proposed.chatContextProvider.d.ts b/src/@types/vscode.proposed.chatContextProvider.d.ts index cf0f1744a4..e7cd493ae5 100644 --- a/src/@types/vscode.proposed.chatContextProvider.d.ts +++ b/src/@types/vscode.proposed.chatContextProvider.d.ts @@ -93,7 +93,6 @@ declare module 'vscode' { /** * An optional command that is executed when the context item is clicked. * The original context item will be passed as the first argument to the command. - * The original context item will be passed as the first argument to the command. */ command?: Command; } @@ -158,7 +157,6 @@ declare module 'vscode' { * `resolveChatContext` is only called for items that do not have a `value`. * * Called when the resource is a webview or a text editor. - * Called when the resource is a webview or a text editor. * * @param options Options include the resource for which to provide context. * @param token A cancellation token. diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index df0f1045c8..603f8a6fcb 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// version: 3 + declare module 'vscode' { export interface ChatParticipant { @@ -96,6 +98,108 @@ declare module 'vscode' { constructor(title: string, message: string | MarkdownString, data: any, buttons?: string[]); } + /** + * An option for a question in a carousel. + */ + export interface ChatQuestionOption { + /** + * Unique identifier for the option. + */ + id: string; + /** + * The display label for the option. + */ + label: string; + /** + * The value returned when this option is selected. + */ + value: unknown; + } + + /** + * The type of question for a chat question carousel. + */ + export enum ChatQuestionType { + /** + * A free-form text input question. + */ + Text = 1, + /** + * A single-select question with radio buttons. + */ + SingleSelect = 2, + /** + * A multi-select question with checkboxes. + */ + MultiSelect = 3 + } + + /** + * A question to be displayed in a question carousel. + */ + export class ChatQuestion { + /** + * Unique identifier for the question. + */ + id: string; + /** + * The type of question: Text for free-form input, SingleSelect for radio buttons, MultiSelect for checkboxes. + */ + type: ChatQuestionType; + /** + * The title/header of the question. + */ + title: string; + /** + * Optional detailed message or description for the question. + */ + message?: string | MarkdownString; + /** + * Options for singleSelect or multiSelect questions. + */ + options?: ChatQuestionOption[]; + /** + * The id(s) of the default selected option(s). + * For SingleSelect, this should be a single option id. + * For MultiSelect, this can be an array of option ids. + */ + defaultValue?: string | string[]; + /** + * Whether to allow free-form text input in addition to predefined options. + * When true, users can provide their own text answer even for SingleSelect or MultiSelect questions. + */ + allowFreeformInput?: boolean; + + constructor( + id: string, + type: ChatQuestionType, + title: string, + options?: { + message?: string | MarkdownString; + options?: ChatQuestionOption[]; + defaultValue?: string | string[]; + allowFreeformInput?: boolean; + } + ); + } + + /** + * A carousel view for presenting multiple questions inline in the chat. + * The UI is displayed but does not block the chat input. + */ + export class ChatResponseQuestionCarouselPart { + /** + * The questions to display in the carousel. + */ + questions: ChatQuestion[]; + /** + * Whether users can skip answering the questions. + */ + allowSkip: boolean; + + constructor(questions: ChatQuestion[], allowSkip?: boolean); + } + export class ChatResponseCodeCitationPart { value: Uri; license: string; @@ -162,6 +266,66 @@ declare module 'vscode' { output: McpToolInvocationContentData[]; } + export enum ChatTodoStatus { + NotStarted = 1, + InProgress = 2, + Completed = 3 + } + + export interface ChatTodoToolInvocationData { + todoList: Array<{ + id: number; + title: string; + status: ChatTodoStatus; + }>; + } + + /** + * Generic tool result data that displays input and output in collapsible sections. + */ + export interface ChatSimpleToolResultData { + /** + * The input to display. + */ + input: string; + /** + * The output to display. + */ + output: string; + } + + + export interface ChatToolResourcesInvocationData { + /** + * Array of file URIs or locations to display as a collapsible list + */ + values: Array; + } + + export class ChatSubagentToolInvocationData { + /** + * A description of the subagent's purpose or task. + */ + description?: string; + + /** + * The name of the subagent being invoked. + */ + agentName?: string; + + /** + * The prompt given to the subagent. + */ + prompt?: string; + + /** + * The result text from the subagent after completion. + */ + result?: string; + + constructor(description?: string, agentName?: string, prompt?: string, result?: string); + } + export class ChatToolInvocationPart { toolName: string; toolCallId: string; @@ -171,11 +335,16 @@ declare module 'vscode' { pastTenseMessage?: string | MarkdownString; isConfirmed?: boolean; isComplete?: boolean; - toolSpecificData?: ChatTerminalToolInvocationData; - fromSubAgent?: boolean; + toolSpecificData?: ChatTerminalToolInvocationData | ChatMcpToolInvocationData | ChatTodoToolInvocationData | ChatSimpleToolResultData | ChatToolResourcesInvocationData | ChatSubagentToolInvocationData; + subAgentInvocationId?: string; presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; - constructor(toolName: string, toolCallId: string, isError?: boolean); + /** + * If this flag is set, this will be treated as an update to any previous tool call with the same id. + */ + enablePartialUpdate?: boolean; + + constructor(toolName: string, toolCallId: string, errorMessage?: string); } /** @@ -244,7 +413,31 @@ declare module 'vscode' { constructor(uris: Uri[], callback: () => Thenable); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; + /** + * Internal type that lists all the proposed chat response parts. This is used to generate `ExtendedChatResponsePart` + * which is the actual type used in this API. This is done so that other proposals can easily add their own response parts + * without having to modify this file. + */ + export interface ExtendedChatResponseParts { + ChatResponsePart: ChatResponsePart; + ChatResponseTextEditPart: ChatResponseTextEditPart; + ChatResponseNotebookEditPart: ChatResponseNotebookEditPart; + ChatResponseWorkspaceEditPart: ChatResponseWorkspaceEditPart; + ChatResponseConfirmationPart: ChatResponseConfirmationPart; + ChatResponseCodeCitationPart: ChatResponseCodeCitationPart; + ChatResponseReferencePart2: ChatResponseReferencePart2; + ChatResponseMovePart: ChatResponseMovePart; + ChatResponseExtensionsPart: ChatResponseExtensionsPart; + ChatResponsePullRequestPart: ChatResponsePullRequestPart; + ChatToolInvocationPart: ChatToolInvocationPart; + ChatResponseMultiDiffPart: ChatResponseMultiDiffPart; + ChatResponseThinkingProgressPart: ChatResponseThinkingProgressPart; + ChatResponseExternalEditPart: ChatResponseExternalEditPart; + ChatResponseQuestionCarouselPart: ChatResponseQuestionCarouselPart; + } + + export type ExtendedChatResponsePart = ExtendedChatResponseParts[keyof ExtendedChatResponseParts]; + export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -348,12 +541,16 @@ declare module 'vscode' { } export class ChatResponsePullRequestPart { - readonly uri: Uri; + /** + * @deprecated use `command` instead + */ + readonly uri?: Uri; + readonly command: Command; readonly linkTag: string; readonly title: string; readonly description: string; readonly author: string; - constructor(uri: Uri, title: string, description: string, author: string, linkTag: string); + constructor(uriOrCommand: Uri | Command, title: string, description: string, author: string, linkTag: string); } export interface ChatResponseStream { @@ -408,6 +605,15 @@ declare module 'vscode' { */ confirmation(title: string, message: string | MarkdownString, data: any, buttons?: string[]): void; + /** + * Show an inline carousel of questions to gather information from the user. + * This is a blocking call that waits for the user to submit or skip the questions. + * @param questions Array of questions to display to the user + * @param allowSkip Whether the user can skip questions without answering + * @returns A promise that resolves with the user's answers, or undefined if skipped + */ + questionCarousel(questions: ChatQuestion[], allowSkip?: boolean): Thenable | undefined>; + /** * Push a warning to this stream. Short-hand for * `push(new ChatResponseWarningPart(message))`. @@ -442,6 +648,13 @@ declare module 'vscode' { push(part: ExtendedChatResponsePart): void; clearToPreviousToolInvocation(reason: ChatResponseClearToPreviousToolInvocationReason): void; + + /** + * Report token usage information for this request. + * This is typically called when the underlying language model provides usage statistics. + * @param usage Token usage information including prompt and completion tokens + */ + usage(usage: ChatResultUsage): void; } export enum ChatResponseReferencePartStatusKind { @@ -633,12 +846,6 @@ declare module 'vscode' { * An optional detail string that will be rendered at the end of the response in certain UI contexts. */ details?: string; - - /** - * Token usage information for this request, if available. - * This is typically provided by the underlying language model. - */ - readonly usage?: ChatResultUsage; } export namespace chat { diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index 4ab722c122..63f9ec2488 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 11 +// version: 14 declare module 'vscode' { @@ -110,6 +110,16 @@ declare module 'vscode' { * Display name of the subagent that is invoking this request. */ readonly subAgentName?: string; + + /** + * The request ID of the parent request that invoked this subagent. + */ + readonly parentRequestId?: string; + + /** + * Whether any hooks are enabled for this request. + */ + readonly hasHooksEnabled: boolean; } export enum ChatRequestEditedFileEventKind { @@ -127,6 +137,10 @@ declare module 'vscode' { * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. */ export class ChatRequestTurn2 { + /** + * The id of the chat request. Used to identity an interaction with any of the chat surfaces. + */ + readonly id?: string; /** * The prompt as entered by the user. * @@ -165,7 +179,7 @@ declare module 'vscode' { /** * @hidden */ - constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined); } export class ChatResponseTurn2 { @@ -244,6 +258,8 @@ declare module 'vscode' { provideFileIgnored(uri: Uri, token: CancellationToken): ProviderResult; } + export type PreToolUsePermissionDecision = 'allow' | 'deny' | 'ask'; + export interface LanguageModelToolInvocationOptions { chatRequestId?: string; /** @deprecated Use {@link chatSessionResource} instead */ @@ -255,6 +271,16 @@ declare module 'vscode' { * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ subAgentInvocationId?: string; + /** + * Pre-tool-use hook result, if the hook was already executed by the caller. + * When provided, the tools service will skip executing its own preToolUse hook + * and use this result for permission decisions and input modifications instead. + */ + preToolUseResult?: { + permissionDecision?: PreToolUsePermissionDecision; + permissionDecisionReason?: string; + updatedInput?: object; + }; } export interface LanguageModelToolInvocationPrepareOptions { @@ -267,6 +293,10 @@ declare module 'vscode' { chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; + /** + * If set, tells the tool that it should include confirmation messages. + */ + forceConfirmationReason?: string; } export interface PreparedToolInvocation { @@ -305,6 +335,19 @@ declare module 'vscode' { export const onDidDisposeChatSession: Event; } + export namespace window { + /** + * The resource URI of the currently active chat panel session, + * or `undefined` if there is no active chat panel session. + */ + export const activeChatPanelSessionResource: Uri | undefined; + + /** + * An event that fires when the active chat panel session resource changes. + */ + export const onDidChangeActiveChatPanelSessionResource: Event; + } + // #endregion // #region ChatErrorDetailsWithConfirmation @@ -339,4 +382,17 @@ declare module 'vscode' { } // #endregion + + // #region Steering + + export interface ChatContext { + /** + * Set to `true` by the editor to request the language model gracefully + * stop after its next opportunity. When set, it's likely that the editor + * will immediately follow up with a new request in the same conversation. + */ + readonly yieldRequested: boolean; + } + + // #endregion } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 84cd547599..c3641a3706 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -23,7 +23,12 @@ declare module 'vscode' { /** * The chat session is currently in progress. */ - InProgress = 2 + InProgress = 2, + + /** + * The chat session needs user input (e.g. an unresolved confirmation). + */ + NeedsInput = 3 } export namespace chat { @@ -42,7 +47,7 @@ declare module 'vscode' { /** * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. */ - export function createChatSessionItemController(id: string, refreshHandler: () => Thenable): ChatSessionItemController; + export function createChatSessionItemController(id: string, refreshHandler: (token: CancellationToken) => Thenable): ChatSessionItemController; } /** @@ -97,7 +102,7 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - refreshHandler: () => Thenable; + readonly refreshHandler: (token: CancellationToken) => Thenable; /** * Fired when an item's archived state changes. @@ -199,54 +204,46 @@ declare module 'vscode' { /** * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */ - created: number; + readonly created: number; /** * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if no requests have been made yet. */ - lastRequestStarted?: number; + readonly lastRequestStarted?: number; /** * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * * Should be undefined if the most recent request is still in progress or if no requests have been made yet. */ - lastRequestEnded?: number; + readonly lastRequestEnded?: number; /** * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `created` and `lastRequestStarted` instead. */ - startTime?: number; + readonly startTime?: number; /** * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. * @deprecated Use `lastRequestEnded` instead. */ - endTime?: number; + readonly endTime?: number; }; /** * Statistics about the chat session. */ - changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[] | { - /** - * Number of files edited during the session. - */ - files: number; - - /** - * Number of insertions made during the session. - */ - insertions: number; + changes?: readonly ChatSessionChangedFile[] | readonly ChatSessionChangedFile2[]; - /** - * Number of deletions made during the session. - */ - deletions: number; - }; + /** + * Arbitrary metadata for the chat session. Can be anything, but must be JSON-stringifyable. + * + * To update the metadata you must re-set this property. + */ + metadata?: { readonly [key: string]: any }; } export class ChatSessionChangedFile { From 40d6d90d12eae54bcbd49e505e20603fb31a2663 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:38:21 +0000 Subject: [PATCH 03/12] feat: add checkout pull request in worktree option Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 15 +++++++ package.nls.json | 1 + src/commands.ts | 113 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/package.json b/package.json index 76d03fb65d..c74f3341e7 100644 --- a/package.json +++ b/package.json @@ -988,6 +988,12 @@ "category": "%command.pull.request.category%", "icon": "$(cloud)" }, + { + "command": "pr.pickInWorktree", + "title": "%command.pr.pickInWorktree.title%", + "category": "%command.pull.request.category%", + "icon": "$(folder-library)" + }, { "command": "pr.exit", "title": "%command.pr.exit.title%", @@ -2059,6 +2065,10 @@ "command": "pr.pickOnCodespaces", "when": "false" }, + { + "command": "pr.pickInWorktree", + "when": "false" + }, { "command": "pr.exit", "when": "github:inReviewMode" @@ -2857,6 +2867,11 @@ "when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && (!isWeb || remoteName != codespaces && virtualWorkspace != vscode-vfs)", "group": "1_pullrequest@3" }, + { + "command": "pr.pickInWorktree", + "when": "view == pr:github && viewItem =~ /pullrequest(:local)?:nonactive/ && !isWeb", + "group": "1_pullrequest@4" + }, { "command": "pr.openChanges", "when": "view =~ /(pr|prStatus):github/ && viewItem =~ /(pullrequest|description)/", diff --git a/package.nls.json b/package.nls.json index 47bceaa46c..e4ec98e12b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -208,6 +208,7 @@ "command.pr.openChanges.title": "Open Changes", "command.pr.pickOnVscodeDev.title": "Checkout Pull Request on vscode.dev", "command.pr.pickOnCodespaces.title": "Checkout Pull Request on Codespaces", + "command.pr.pickInWorktree.title": "Checkout Pull Request in Worktree", "command.pr.exit.title": "Checkout Default Branch", "command.pr.dismissNotification.title": "Dismiss Notification", "command.pr.markAllCopilotNotificationsAsRead.title": "Dismiss All Copilot Notifications", diff --git a/src/commands.ts b/src/commands.ts index 88e3784b06..334a198d32 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -825,6 +825,119 @@ export function registerCommands( ), ); + context.subscriptions.push( + vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { + if (pr === undefined) { + Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId); + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.')); + } + + let pullRequestModel: PullRequestModel; + let repository: Repository | undefined; + + if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) { + pullRequestModel = pr.pullRequestModel; + repository = pr.repository; + } else { + pullRequestModel = pr; + } + + // Validate that the PR has a valid head branch + if (!pullRequestModel.head) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.')); + } + + // Get the folder manager to access the repository + const folderManager = reposManager.getManagerForIssueModel(pullRequestModel); + if (!folderManager) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.')); + } + + const repositoryToUse = repository || folderManager.repository; + + /* __GDPR__ + "pr.checkoutInWorktree" : {} + */ + telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Checking out Pull Request #{0} in worktree', pullRequestModel.number), + }, + async (progress) => { + // Generate a branch name for the worktree + const branchName = pullRequestModel.head!.ref; + const remoteName = pullRequestModel.remote.remoteName; + + // Fetch the PR branch first + progress.report({ message: vscode.l10n.t('Fetching branch {0}...', branchName) }); + try { + await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); + } catch (e) { + Logger.appendLine(`Failed to fetch branch ${branchName}: ${e}`, logId); + // Continue even if fetch fails - the branch might already be available locally + } + + // Ask user for worktree location + const repoRootPath = repositoryToUse.rootUri.fsPath; + const parentDir = pathLib.dirname(repoRootPath); + const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); + + const worktreeUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }); + + if (!worktreeUri) { + return; // User cancelled + } + + const worktreePath = worktreeUri.fsPath; + + // Create the worktree using git command + progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); + + const trackedBranchName = `${remoteName}/${branchName}`; + const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; + + try { + // Execute git worktree add command + const terminal = vscode.window.createTerminal({ + name: vscode.l10n.t('Git Worktree'), + cwd: repoRootPath, + hideFromUser: true, + }); + + // Create worktree with a new local branch tracking the remote + terminal.sendText(`git worktree add -b "${localBranchName}" "${worktreePath}" "${trackedBranchName}" && exit`); + + // Wait a bit for the command to complete + await new Promise(resolve => setTimeout(resolve, 2000)); + + terminal.dispose(); + + // Ask user if they want to open the worktree + const openAction = vscode.l10n.t('Open in New Window'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), + openAction + ); + + if (result === openAction) { + await commands.openFolder(worktreeUri, { forceNewWindow: true }); + } + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.error(`Failed to create worktree: ${errorMessage}`, logId); + return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); + } + } + ); + }), + ); + context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: BaseContext | undefined) => { if (!context) { return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.')); From ecc650ddd3d527c459648bde54051e0892350576 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:42:25 +0000 Subject: [PATCH 04/12] fix: use VS Code Tasks API for reliable cross-platform worktree creation Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 334a198d32..6162a4d18f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -896,27 +896,51 @@ export function registerCommands( const worktreePath = worktreeUri.fsPath; - // Create the worktree using git command + // Create the worktree using a VS Code task progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; try { - // Execute git worktree add command - const terminal = vscode.window.createTerminal({ - name: vscode.l10n.t('Git Worktree'), - cwd: repoRootPath, - hideFromUser: true, + // Create a VS Code task to execute the git worktree command + const taskDefinition: vscode.TaskDefinition = { + type: 'shell' + }; + + const shellExecution = new vscode.ShellExecution('git', [ + 'worktree', 'add', + '-b', { value: localBranchName, quoting: vscode.ShellQuoting.Strong }, + { value: worktreePath, quoting: vscode.ShellQuoting.Strong }, + { value: trackedBranchName, quoting: vscode.ShellQuoting.Strong } + ], { + cwd: repoRootPath }); - // Create worktree with a new local branch tracking the remote - terminal.sendText(`git worktree add -b "${localBranchName}" "${worktreePath}" "${trackedBranchName}" && exit`); - - // Wait a bit for the command to complete - await new Promise(resolve => setTimeout(resolve, 2000)); + const task = new vscode.Task( + taskDefinition, + vscode.TaskScope.Workspace, + vscode.l10n.t('Create Worktree for Pull Request #{0}', pullRequestModel.number), + 'git', + shellExecution + ); - terminal.dispose(); + // Execute the task and wait for completion + const taskExecution = await vscode.tasks.executeTask(task); + + // Wait for task to complete + await new Promise((resolve, reject) => { + const disposable = vscode.tasks.onDidEndTaskProcess(e => { + if (e.execution === taskExecution) { + disposable.dispose(); + if (e.exitCode === 0) { + resolve(); + } else { + reject(new Error(vscode.l10n.t('Git worktree command failed with exit code {0}', e.exitCode?.toString() ?? 'unknown'))); + } + } + }); + }); // Ask user if they want to open the worktree const openAction = vscode.l10n.t('Open in New Window'); From fab1204c8d71649c812651b970736e1777f15070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:44:10 +0000 Subject: [PATCH 05/12] fix: address code review feedback - improve error handling and branch naming Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6162a4d18f..6fee5bf5e6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -847,6 +847,9 @@ export function registerCommands( return vscode.window.showErrorMessage(vscode.l10n.t('Unable to checkout pull request: missing head branch information.')); } + // Store validated head to avoid non-null assertions later + const prHead = pullRequestModel.head; + // Get the folder manager to access the repository const folderManager = reposManager.getManagerForIssueModel(pullRequestModel); if (!folderManager) { @@ -867,7 +870,7 @@ export function registerCommands( }, async (progress) => { // Generate a branch name for the worktree - const branchName = pullRequestModel.head!.ref; + const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; // Fetch the PR branch first @@ -875,7 +878,8 @@ export function registerCommands( try { await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); } catch (e) { - Logger.appendLine(`Failed to fetch branch ${branchName}: ${e}`, logId); + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); // Continue even if fetch fails - the branch might already be available locally } @@ -900,7 +904,7 @@ export function registerCommands( progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = `pr-${pullRequestModel.number}/${branchName}`; + const localBranchName = `pr-${pullRequestModel.number}-${branchName}`; try { // Create a VS Code task to execute the git worktree command From 80e46a2571c09fe667d7c01d740a68f4a326a089 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:40:23 +0100 Subject: [PATCH 06/12] argument clean up and keep branch name --- src/commands.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6fee5bf5e6..ccf90c2f75 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -826,8 +826,8 @@ export function registerCommands( ); context.subscriptions.push( - vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { - if (pr === undefined) { + vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | unknown) => { + if (pr === undefined || !(pr instanceof PRNode)) { Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId); return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.')); } @@ -835,12 +835,8 @@ export function registerCommands( let pullRequestModel: PullRequestModel; let repository: Repository | undefined; - if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) { - pullRequestModel = pr.pullRequestModel; - repository = pr.repository; - } else { - pullRequestModel = pr; - } + pullRequestModel = pr.pullRequestModel; + repository = pr.repository; // Validate that the PR has a valid head branch if (!pullRequestModel.head) { @@ -904,7 +900,7 @@ export function registerCommands( progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = `pr-${pullRequestModel.number}-${branchName}`; + const localBranchName = branchName; try { // Create a VS Code task to execute the git worktree command From 103da71cff1d312beddadcdc7aca5223a827f15d Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:45:55 +0100 Subject: [PATCH 07/12] Update git API --- src/@types/git.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/@types/git.d.ts b/src/@types/git.d.ts index 718d8f4b6b..7e4aebbced 100644 --- a/src/@types/git.d.ts +++ b/src/@types/git.d.ts @@ -287,6 +287,9 @@ export interface Repository { applyStash(index?: number): Promise; popStash(index?: number): Promise; dropStash(index?: number): Promise; + + createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree(path: string, options?: { force?: boolean }): Promise; } export interface RemoteSource { From 04c416a09a3a4ebd8a1a877820df582130eb6f5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:51:45 +0000 Subject: [PATCH 08/12] refactor: use git extension API for worktree creation instead of shell execution Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/api/api.d.ts | 3 +++ src/commands.ts | 48 ++++++++++-------------------------------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/api/api.d.ts b/src/api/api.d.ts index 011cbd9cfa..68ed7b79eb 100644 --- a/src/api/api.d.ts +++ b/src/api/api.d.ts @@ -209,6 +209,9 @@ export interface Repository { add(paths: string[]): Promise; merge(ref: string): Promise; mergeAbort(): Promise; + + createWorktree?(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree?(path: string, options?: { force?: boolean }): Promise; } /** diff --git a/src/commands.ts b/src/commands.ts index ccf90c2f75..196377a1a3 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -896,50 +896,22 @@ export function registerCommands( const worktreePath = worktreeUri.fsPath; - // Create the worktree using a VS Code task + // Create the worktree using the git extension API progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); const trackedBranchName = `${remoteName}/${branchName}`; - const localBranchName = branchName; try { - // Create a VS Code task to execute the git worktree command - const taskDefinition: vscode.TaskDefinition = { - type: 'shell' - }; - - const shellExecution = new vscode.ShellExecution('git', [ - 'worktree', 'add', - '-b', { value: localBranchName, quoting: vscode.ShellQuoting.Strong }, - { value: worktreePath, quoting: vscode.ShellQuoting.Strong }, - { value: trackedBranchName, quoting: vscode.ShellQuoting.Strong } - ], { - cwd: repoRootPath - }); - - const task = new vscode.Task( - taskDefinition, - vscode.TaskScope.Workspace, - vscode.l10n.t('Create Worktree for Pull Request #{0}', pullRequestModel.number), - 'git', - shellExecution - ); + // Check if the createWorktree API is available + if (!repositoryToUse.createWorktree) { + throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); + } - // Execute the task and wait for completion - const taskExecution = await vscode.tasks.executeTask(task); - - // Wait for task to complete - await new Promise((resolve, reject) => { - const disposable = vscode.tasks.onDidEndTaskProcess(e => { - if (e.execution === taskExecution) { - disposable.dispose(); - if (e.exitCode === 0) { - resolve(); - } else { - reject(new Error(vscode.l10n.t('Git worktree command failed with exit code {0}', e.exitCode?.toString() ?? 'unknown'))); - } - } - }); + // Use the git extension's createWorktree API + await repositoryToUse.createWorktree({ + path: worktreePath, + commitish: trackedBranchName, + branch: branchName }); // Ask user if they want to open the worktree From ad7444526b565c573a102a398956f982c9013b1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:06:27 +0000 Subject: [PATCH 09/12] fix: run fetch and worktree selection in parallel, move info message after progress Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 106 ++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 196377a1a3..c51df0dd7e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -859,18 +859,17 @@ export function registerCommands( */ telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: vscode.l10n.t('Checking out Pull Request #{0} in worktree', pullRequestModel.number), - }, - async (progress) => { - // Generate a branch name for the worktree - const branchName = prHead.ref; - const remoteName = pullRequestModel.remote.remoteName; - - // Fetch the PR branch first - progress.report({ message: vscode.l10n.t('Fetching branch {0}...', branchName) }); + // Prepare for parallel operations + const repoRootPath = repositoryToUse.rootUri.fsPath; + const parentDir = pathLib.dirname(repoRootPath); + const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); + const branchName = prHead.ref; + const remoteName = pullRequestModel.remote.remoteName; + + // Run fetch and worktree location selection in parallel + const [, worktreeUri] = await Promise.all([ + // Fetch the PR branch + (async () => { try { await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); } catch (e) { @@ -878,59 +877,62 @@ export function registerCommands( Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); // Continue even if fetch fails - the branch might already be available locally } + })(), + // Ask user for worktree location + vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }) + ]); - // Ask user for worktree location - const repoRootPath = repositoryToUse.rootUri.fsPath; - const parentDir = pathLib.dirname(repoRootPath); - const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); - - const worktreeUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(defaultWorktreePath), - title: vscode.l10n.t('Select Worktree Location'), - saveLabel: vscode.l10n.t('Create Worktree'), - }); - - if (!worktreeUri) { - return; // User cancelled - } + if (!worktreeUri) { + return; // User cancelled + } - const worktreePath = worktreeUri.fsPath; + const worktreePath = worktreeUri.fsPath; + const trackedBranchName = `${remoteName}/${branchName}`; - // Create the worktree using the git extension API - progress.report({ message: vscode.l10n.t('Creating worktree at {0}...', worktreePath) }); + try { + // Check if the createWorktree API is available + if (!repositoryToUse.createWorktree) { + throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); + } - const trackedBranchName = `${remoteName}/${branchName}`; - - try { - // Check if the createWorktree API is available - if (!repositoryToUse.createWorktree) { - throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); - } + // Store reference to ensure type narrowing + const createWorktree = repositoryToUse.createWorktree; + // Create the worktree with progress + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number), + }, + async () => { // Use the git extension's createWorktree API - await repositoryToUse.createWorktree({ + await createWorktree({ path: worktreePath, commitish: trackedBranchName, branch: branchName }); + } + ); - // Ask user if they want to open the worktree - const openAction = vscode.l10n.t('Open in New Window'); - const result = await vscode.window.showInformationMessage( - vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), - openAction - ); + // Ask user if they want to open the worktree (after progress is finished) + const openAction = vscode.l10n.t('Open in New Window'); + const result = await vscode.window.showInformationMessage( + vscode.l10n.t('Worktree created for Pull Request #{0}', pullRequestModel.number), + openAction + ); - if (result === openAction) { - await commands.openFolder(worktreeUri, { forceNewWindow: true }); - } - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.error(`Failed to create worktree: ${errorMessage}`, logId); - return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); - } + if (result === openAction) { + await commands.openFolder(worktreeUri, { forceNewWindow: true }); } - ); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.error(`Failed to create worktree: ${errorMessage}`, logId); + return vscode.window.showErrorMessage(vscode.l10n.t('Failed to create worktree: {0}', errorMessage)); + } }), ); From 3e1c44ebc26887d64ed04edde0feeb057f975d73 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:07:26 +0100 Subject: [PATCH 10/12] Fix createWorktree call --- src/commands.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index c51df0dd7e..67b3d21503 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -899,9 +899,6 @@ export function registerCommands( throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); } - // Store reference to ensure type narrowing - const createWorktree = repositoryToUse.createWorktree; - // Create the worktree with progress await vscode.window.withProgress( { @@ -910,7 +907,7 @@ export function registerCommands( }, async () => { // Use the git extension's createWorktree API - await createWorktree({ + await repositoryToUse.createWorktree!({ path: worktreePath, commitish: trackedBranchName, branch: branchName From 766d8a006fac77f74c9e6eeea20b3e81ba206a06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:12:37 +0000 Subject: [PATCH 11/12] fix: start progress before fetch operation, include fetch in progress scope Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/commands.ts | 38 +++++++++++++++++--------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 67b3d21503..8999e6ed1f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -859,32 +859,19 @@ export function registerCommands( */ telemetry.sendTelemetryEvent('pr.checkoutInWorktree'); - // Prepare for parallel operations + // Prepare for operations const repoRootPath = repositoryToUse.rootUri.fsPath; const parentDir = pathLib.dirname(repoRootPath); const defaultWorktreePath = pathLib.join(parentDir, `pr-${pullRequestModel.number}`); const branchName = prHead.ref; const remoteName = pullRequestModel.remote.remoteName; - // Run fetch and worktree location selection in parallel - const [, worktreeUri] = await Promise.all([ - // Fetch the PR branch - (async () => { - try { - await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); - // Continue even if fetch fails - the branch might already be available locally - } - })(), - // Ask user for worktree location - vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(defaultWorktreePath), - title: vscode.l10n.t('Select Worktree Location'), - saveLabel: vscode.l10n.t('Create Worktree'), - }) - ]); + // Ask user for worktree location first (not in progress) + const worktreeUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(defaultWorktreePath), + title: vscode.l10n.t('Select Worktree Location'), + saveLabel: vscode.l10n.t('Create Worktree'), + }); if (!worktreeUri) { return; // User cancelled @@ -899,13 +886,22 @@ export function registerCommands( throw new Error(vscode.l10n.t('Git worktree API is not available. Please update VS Code to the latest version.')); } - // Create the worktree with progress + // Start progress for fetch and worktree creation await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Creating worktree for Pull Request #{0}...', pullRequestModel.number), }, async () => { + // Fetch the PR branch first + try { + await repositoryToUse.fetch({ remote: remoteName, ref: branchName }); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + Logger.appendLine(`Failed to fetch branch ${branchName}: ${errorMessage}`, logId); + // Continue even if fetch fails - the branch might already be available locally + } + // Use the git extension's createWorktree API await repositoryToUse.createWorktree!({ path: worktreePath, From fa08fb76d2daf06d1c38ba98dfe04c387223ac9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:04:54 +0000 Subject: [PATCH 12/12] feat: add 'Checkout in Worktree' option to PR Description checkout button Agent-Logs-Url: https://github.com/microsoft/vscode-pull-request-github/sessions/113a0bba-a0a7-47d9-baca-dd4dd5fda645 --- ...ode.proposed.chatParticipantAdditions.d.ts | 28 ++- ...scode.proposed.chatParticipantPrivate.d.ts | 53 +++- .../vscode.proposed.chatSessionsProvider.d.ts | 232 +++++++++++++++--- src/commands.ts | 15 +- src/github/pullRequestOverview.ts | 13 + webviews/common/context.tsx | 2 + webviews/components/header.tsx | 10 +- 7 files changed, 308 insertions(+), 45 deletions(-) diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 603f8a6fcb..f38544f604 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -223,6 +223,16 @@ declare module 'vscode' { }; language: string; + /** + * Overrides for how the command is presented in the UI. + * For example, when a `cd && ` prefix is detected, + * the presentation can show only the actual command. + */ + presentationOverrides?: { + commandLine: string; + language?: string; + }; + /** * Terminal command output. Displayed when the terminal is no longer available. */ @@ -829,6 +839,12 @@ declare module 'vscode' { */ readonly completionTokens: number; + /** + * The number of tokens reserved for the response. + * This is rendered specially in the UI to indicate that these tokens aren't used but are reserved. + */ + readonly outputBuffer?: number; + /** * Optional breakdown of prompt token usage by category and label. * If the percentages do not sum to 100%, the remaining will be shown as "Uncategorized". @@ -974,10 +990,6 @@ declare module 'vscode' { readonly toolReferences?: readonly ChatLanguageModelToolReference[]; } - export interface ChatResultFeedback { - readonly unhelpfulReason?: string; - } - export namespace lm { export function fileIsIgnored(uri: Uri, token?: CancellationToken): Thenable; } @@ -1020,8 +1032,6 @@ declare module 'vscode' { readonly rawInput?: unknown; readonly chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - readonly chatSessionId?: string; readonly chatSessionResource?: Uri; readonly chatInteractionId?: string; } @@ -1051,9 +1061,15 @@ declare module 'vscode' { } export interface ChatRequestModeInstructions { + /** set when the mode a custom agent (not built-in), to be used as identifier */ + readonly uri?: Uri; readonly name: string; readonly content: string; readonly toolReferences?: readonly ChatLanguageModelToolReference[]; readonly metadata?: Record; + /** + * Whether the mode is a builtin mode (e.g. Ask, Edit, Agent) rather than a user or extension-defined custom mode. + */ + readonly isBuiltin?: boolean; } } diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index 63f9ec2488..5e354883f7 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 14 +// version: 15 declare module 'vscode' { @@ -116,10 +116,25 @@ declare module 'vscode' { */ readonly parentRequestId?: string; + /** + * The permission level for tool auto-approval in this request. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. + */ + readonly permissionLevel?: string; + /** * Whether any hooks are enabled for this request. */ readonly hasHooksEnabled: boolean; + + /** + * When true, this request was initiated by the system (e.g. a terminal + * command completion notification) rather than by the user typing a + * message. Extensions can use this to render the prompt differently + * and skip billing. + */ + readonly isSystemInitiated?: boolean; } export enum ChatRequestEditedFileEventKind { @@ -176,10 +191,20 @@ declare module 'vscode' { */ readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + /** + * The identifier of the language model that was used for this request, if known. + */ + readonly modelId?: string; + + /** + * The mode instructions that were active for this request, if any. + */ + readonly modeInstructions2?: ChatRequestModeInstructions; + /** * @hidden */ - constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined, id: string | undefined, modelId: string | undefined, modeInstructions2: ChatRequestModeInstructions | undefined); } export class ChatResponseTurn2 { @@ -262,8 +287,6 @@ declare module 'vscode' { export interface LanguageModelToolInvocationOptions { chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; terminalCommand?: string; @@ -289,8 +312,6 @@ declare module 'vscode' { */ input: T; chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; /** @@ -392,7 +413,27 @@ declare module 'vscode' { * will immediately follow up with a new request in the same conversation. */ readonly yieldRequested: boolean; + + /** + * The resource URI identifying the chat session this context belongs to. + * Available when the context is provided for title generation, summarization, + * or other session-scoped operations. Extracted from the session's history entries. + */ + readonly sessionResource?: Uri; } // #endregion + + export interface LanguageModelToolInformation { + /** + * The full reference name of this tool as used in agent definition files. + * + * For MCP tools, this is the canonical name in the format `serverShortName/toolReferenceName` + * (e.g., `github/search_issues`). This can be used to map between the tool names specified + * in agent `.md` files and the tool's internal {@link LanguageModelToolInformation.name id}. + * + * This property is only set for MCP tools. For other tool types, it is `undefined`. + */ + readonly fullReferenceName?: string; + } } diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index c3641a3706..06d0649b18 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -35,6 +35,8 @@ declare module 'vscode' { /** * Registers a new {@link ChatSessionItemProvider chat session item provider}. * + * @deprecated Use {@linkcode createChatSessionItemController} instead. + * * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. * * @param chatSessionType The type of chat session the provider is for. @@ -46,12 +48,21 @@ declare module 'vscode' { /** * Creates a new {@link ChatSessionItemController chat session item controller} with the given unique identifier. + * + * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. + * + * @param chatSessionType The type of chat session the provider is for. + * @param refreshHandler The controller's {@link ChatSessionItemController.refreshHandler refresh handler}. + * + * @returns A new controller instance that can be used to manage chat session items for the given chat session type. */ - export function createChatSessionItemController(id: string, refreshHandler: (token: CancellationToken) => Thenable): ChatSessionItemController; + export function createChatSessionItemController(chatSessionType: string, refreshHandler: ChatSessionItemControllerRefreshHandler): ChatSessionItemController; } /** * Provides a list of information about chat sessions. + * + * @deprecated Use {@linkcode ChatSessionItemController} instead. */ export interface ChatSessionItemProvider { /** @@ -77,7 +88,64 @@ declare module 'vscode' { } /** - * Provides a list of information about chat sessions. + * Extension callback invoked to refresh the collection of chat session items for a {@linkcode ChatSessionItemController}. + */ + export type ChatSessionItemControllerRefreshHandler = (token: CancellationToken) => Thenable; + + export interface ChatSessionItemControllerNewItemHandlerContext { + // TODO: Use a better type but for now decrease this down to just the prompt and command since that's all we currently need. + // The problem with ChatRequest is that it has a resourceUri which is not good for this code path. + readonly request: { + readonly prompt: string; + readonly command?: string; + }; + + /** + * @deprecated Use `inputState` instead + */ + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; + + readonly inputState: ChatSessionInputState; + } + + /** + * Extension callback invoked when a new chat session is started. + */ + export type ChatSessionItemControllerNewItemHandler = (context: ChatSessionItemControllerNewItemHandlerContext, token: CancellationToken) => Thenable; + + /** + * Extension callback invoked to get the input state for a chat session. + * + * @param sessionResource The resource of the chat session to get the input state for. `undefined` indicates this is + * for a blank chat editor that is not yet associated with a session. + * @param context Additional context + * @param token Cancellation token. + * + * @return A new chat session input state. This should be created using {@link ChatSessionItemController.createChatSessionInputState}. + */ + export type ChatSessionControllerGetInputState = (sessionResource: Uri | undefined, context: { + /** + * The previous input state for the session. + */ + readonly previousInputState: ChatSessionInputState | undefined; + }, token: CancellationToken) => Thenable | ChatSessionInputState; + + /** + * Extension callback invoked to fork an existing chat session item managed by a {@linkcode ChatSessionItemController}. + * + * The handler should create a new session on the provider's backend and + * return the new {@link ChatSessionItem} representing the forked session. + * + * @param sessionResource The resource of the chat session being forked. + * @param request The request turn that marks the fork point. The forked session includes all turns + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. + * @param token A cancellation token. + * @returns The forked session item. + */ + export type ChatSessionItemControllerForkHandler = (sessionResource: Uri, request: ChatRequestTurn2 | undefined, token: CancellationToken) => Thenable | ChatSessionItem; + + /** + * Manages chat sessions for a specific chat session type */ export interface ChatSessionItemController { readonly id: string; @@ -93,7 +161,7 @@ declare module 'vscode' { readonly items: ChatSessionItemCollection; /** - * Creates a new managed chat session item that be added to the collection. + * Creates a new managed chat session item that can be added to the collection. */ createChatSessionItem(resource: Uri, label: string): ChatSessionItem; @@ -102,12 +170,39 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - readonly refreshHandler: (token: CancellationToken) => Thenable; + readonly refreshHandler: ChatSessionItemControllerRefreshHandler; /** * Fired when an item's archived state changes. */ readonly onDidChangeChatSessionItemState: Event; + + /** + * Invoked when a new chat session is started. + * + * This allows the controller to initialize the chat session item with information from the initial request. + * + * The returned chat session is added to the collection and shown in the UI. + */ + newChatSessionItemHandler?: ChatSessionItemControllerNewItemHandler; + + /** + * Invoked when an existing chat session is forked. + * + * When both this handler and {@linkcode ChatSession.forkHandler} are registered, + * this handler takes precedence. + */ + forkHandler?: ChatSessionItemControllerForkHandler; + + /** + * Gets the input state for a chat session. + */ + getChatSessionInputState?: ChatSessionControllerGetInputState; + + /** + * Create a new managed ChatSessionInputState object. + */ + createChatSessionInputState(groups: ChatSessionProviderOptionGroup[]): ChatSessionInputState; } /** @@ -121,7 +216,8 @@ declare module 'vscode' { /** * Replaces the items stored by the collection. - * @param items Items to store. + * + * @param items Items to store. If two items have the same resource URI, the last one will be used. */ replace(items: readonly ChatSessionItem[]): void; @@ -136,31 +232,42 @@ declare module 'vscode' { /** * Adds the chat session item to the collection. If an item with the same resource URI already * exists, it'll be replaced. + * * @param item Item to add. */ add(item: ChatSessionItem): void; /** * Removes a single chat session item from the collection. + * * @param resource Item resource to delete. */ delete(resource: Uri): void; /** * Efficiently gets a chat session item by resource, if it exists, in the collection. + * * @param resource Item resource to get. + * * @returns The found item or undefined if it does not exist. */ get(resource: Uri): ChatSessionItem | undefined; } + /** + * A chat session show in the UI. + * + * This should be created by calling a {@link ChatSessionItemController.createChatSessionItem createChatSessionItem} + * method on the controller. The item can then be added to the controller's {@link ChatSessionItemController.items items collection} + * to show it in the UI. + */ export interface ChatSessionItem { /** * The resource associated with the chat session. * * This is uniquely identifies the chat session and is used to open the chat session. */ - resource: Uri; + readonly resource: Uri; /** * Human readable name of the session shown in the UI @@ -246,6 +353,9 @@ declare module 'vscode' { metadata?: { readonly [key: string]: any }; } + /** + * @deprecated Use `ChatSessionChangedFile2` instead + */ export class ChatSessionChangedFile { /** * URI of the file. @@ -300,6 +410,15 @@ declare module 'vscode' { } export interface ChatSession { + /** + * An optional title for the chat session. + * + * When provided, this title is used as the display name for the session + * (e.g. in the editor tab). When not provided, the title defaults to + * the first user message in the session history. + */ + readonly title?: string; + /** * The full history of the session * @@ -335,7 +454,24 @@ declare module 'vscode' { */ // TODO: Should we introduce our own type for `ChatRequestHandler` since not all field apply to chat sessions? // TODO: Revisit this to align with code. + // TODO: pass in options? readonly requestHandler: ChatRequestHandler | undefined; + + /** + * Handles a request to fork the session. + * + * The handler should create a new session on the provider's backend and + * return the new {@link ChatSessionItem} representing the forked session. + * + * @deprecated Use {@linkcode ChatSessionItemController.forkHandler} instead. This remains supported for backwards compatibility. + * + * @param sessionResource The resource of the chat session being forked. + * @param request The request turn that marks the fork point. The forked session includes all turns + * upto this request turn and excludes this request turn itself. If undefined, fork the full session. + * @param token A cancellation token. + * @returns The forked session item. + */ + readonly forkHandler?: ChatSessionItemControllerForkHandler; } /** @@ -367,11 +503,15 @@ declare module 'vscode' { */ export interface ChatSessionContentProvider { /** + * @deprecated + * * Event that the provider can fire to signal that the options for a chat session have changed. */ readonly onDidChangeChatSessionOptions?: Event; /** + * @deprecated + * * Event that the provider can fire to signal that the available provider options have changed. * * When fired, the editor will re-query {@link ChatSessionContentProvider.provideChatSessionProviderOptions} @@ -386,12 +526,22 @@ declare module 'vscode' { * * @param resource The URI of the chat session to resolve. * @param token A cancellation token that can be used to cancel the operation. + * @param context Additional context for the chat session. * * @return The {@link ChatSession chat session} associated with the given URI. */ - provideChatSessionContent(resource: Uri, token: CancellationToken): Thenable | ChatSession; + provideChatSessionContent(resource: Uri, token: CancellationToken, context: { + readonly inputState: ChatSessionInputState; + + /** + * @deprecated Use `inputState` instead + */ + readonly sessionOptions: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; + }): Thenable | ChatSession; /** + * @deprecated + * * @param resource Identifier of the chat session being updated. * @param updates Collection of option identifiers and their new values. Only the options that changed are included. * @param token A cancellation token that can be used to cancel the notification if the session is disposed. @@ -399,10 +549,11 @@ declare module 'vscode' { provideHandleOptionsChange?(resource: Uri, updates: ReadonlyArray, token: CancellationToken): void; /** + * @deprecated + * * Called as soon as you register (call me once) - * @param token */ - provideChatSessionProviderOptions?(token: CancellationToken): Thenable; + provideChatSessionProviderOptions?(token: CancellationToken): Thenable; } export interface ChatSessionOptionUpdate { @@ -423,10 +574,11 @@ declare module 'vscode' { * * @param scheme The uri-scheme to register for. This must be unique. * @param provider The provider to register. + * @param defaultChatParticipant The default {@link ChatParticipant chat participant} used in sessions provided by this provider. * * @returns A disposable that unregisters the provider when disposed. */ - export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; + export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, defaultChatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; } export interface ChatContext { @@ -435,7 +587,15 @@ declare module 'vscode' { export interface ChatSessionContext { readonly chatSessionItem: ChatSessionItem; // Maps to URI of chat session editor (could be 'untitled-1', etc..) + + /** @deprecated This will be removed along with the concept of `untitled-` sessions. */ readonly isUntitled: boolean; + + /** + * The initial option selections for the session, provided with the first request. + * Contains the options the user selected (or defaults) before the session was created. + */ + readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; } export interface ChatSessionCapabilities { @@ -502,10 +662,15 @@ declare module 'vscode' { */ readonly description?: string; + /** + * The currently selected option for this group. This must be one of the items provided in the `items` array. + */ + readonly selected?: ChatSessionProviderOptionItem; + /** * The selectable items within this option group. */ - readonly items: ChatSessionProviderOptionItem[]; + readonly items: readonly ChatSessionProviderOptionItem[]; /** * A context key expression that controls when this option group picker is visible. @@ -517,31 +682,18 @@ declare module 'vscode' { */ readonly when?: string; - /** - * When true, displays a searchable QuickPick with a "See more..." option. - * Recommended for option groups with additional async items (e.g., repositories). - */ - readonly searchable?: boolean; - /** * An icon for the option group shown in UI. */ readonly icon?: ThemeIcon; - /** - * Handler for dynamic search when `searchable` is true. - * Called when the user types in the searchable QuickPick or clicks "See more..." to load additional items. - * - * @param query The search query entered by the user. Empty string for initial load. - * @param token A cancellation token. - * @returns Additional items to display in the searchable QuickPick. - */ - readonly onSearch?: (query: string, token: CancellationToken) => Thenable; - /** * Optional commands. * * These commands will be displayed at the bottom of the group. + * + * For extensions that use the new `provideChatSessionInputState` API, these commands are passed a context object + * `{ inputState: ChatSessionInputState; sessionResource: Uri | undefined }` that they can use to determine which session and options they are being invoked for. */ readonly commands?: Command[]; } @@ -551,6 +703,30 @@ declare module 'vscode' { * Provider-defined option groups (0-2 groups supported). * Examples: models picker, sub-agents picker, etc. */ - optionGroups?: ChatSessionProviderOptionGroup[]; + readonly optionGroups?: readonly ChatSessionProviderOptionGroup[]; + + /** + * The set of default options used for new chat sessions, provided as key-value pairs. + * + * Keys correspond to option group IDs (e.g., 'models', 'subagents'). + */ + readonly newSessionOptions?: Record; + } + + /** + * Represents the current state of user inputs for a chat session. + */ + export interface ChatSessionInputState { + /** + * Fired when the input state is changed by the user. + */ + readonly onDidChange: Event; + + /** + * The groups of options to show in the UI for user input. + * + * To update the groups you must replace the entire `groups` array with a new array. + */ + groups: readonly ChatSessionProviderOptionGroup[]; } } diff --git a/src/commands.ts b/src/commands.ts index 8999e6ed1f..1b5cacb621 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -826,8 +826,8 @@ export function registerCommands( ); context.subscriptions.push( - vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | unknown) => { - if (pr === undefined || !(pr instanceof PRNode)) { + vscode.commands.registerCommand('pr.pickInWorktree', async (pr: PRNode | PullRequestModel | unknown) => { + if (pr === undefined) { Logger.error('Unexpectedly received undefined when picking a PR for worktree checkout.', logId); return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.')); } @@ -835,8 +835,15 @@ export function registerCommands( let pullRequestModel: PullRequestModel; let repository: Repository | undefined; - pullRequestModel = pr.pullRequestModel; - repository = pr.repository; + if (pr instanceof PRNode) { + pullRequestModel = pr.pullRequestModel; + repository = pr.repository; + } else if (pr instanceof PullRequestModel) { + pullRequestModel = pr; + } else { + Logger.error('Unexpectedly received unknown type when picking a PR for worktree checkout.', logId); + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.')); + } // Validate that the PR has a valid head branch if (!pullRequestModel.head) { diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 66b06e4b7d..47b381fe2d 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -471,6 +471,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): void { + vscode.commands.executeCommand('pr.pickInWorktree', this._item).then( + () => { + this._replyMessage(message, {}); + }, + () => { + this._replyMessage(message, {}); + }, + ); + } + private async mergePullRequest( message: IRequestMessage, ): Promise { diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx index 55cb61836f..205791f57e 100644 --- a/webviews/common/context.tsx +++ b/webviews/common/context.tsx @@ -34,6 +34,8 @@ export class PRContext { public checkout = () => this.postMessage({ command: 'pr.checkout' }); + public checkoutInWorktree = () => this.postMessage({ command: 'pr.checkout-in-worktree' }); + public openChanges = (openToTheSide?: boolean) => this.postMessage({ command: 'pr.open-changes', args: { openToTheSide } }); public copyPrLink = () => this.postMessage({ command: 'pr.copy-prlink' }); diff --git a/webviews/components/header.tsx b/webviews/components/header.tsx index 8fc8592f6d..e01e2e031f 100644 --- a/webviews/components/header.tsx +++ b/webviews/components/header.tsx @@ -306,7 +306,7 @@ interface CheckoutButtonProps { } const CheckoutButton: React.FC = ({ isCurrentlyCheckedOut, isIssue, doneCheckoutBranch, owner, repo, number }) => { - const { exitReviewMode, checkout, openChanges } = useContext(PullRequestContext); + const { exitReviewMode, checkout, checkoutInWorktree, openChanges } = useContext(PullRequestContext); const [isBusy, setBusy] = useState(false); const onClick = async (command: string) => { @@ -317,6 +317,9 @@ const CheckoutButton: React.FC = ({ isCurrentlyCheckedOut, case 'checkout': await checkout(); break; + case 'checkoutInWorktree': + await checkoutInWorktree(); + break; case 'exitReviewMode': await exitReviewMode(); break; @@ -357,6 +360,11 @@ const CheckoutButton: React.FC = ({ isCurrentlyCheckedOut, value: '', action: () => onClick('checkout') }); + actions.push({ + label: 'Checkout in Worktree', + value: '', + action: () => onClick('checkoutInWorktree') + }); } actions.push({