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/@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 { 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..f38544f604 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; @@ -119,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. */ @@ -162,6 +276,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 +345,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 +423,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 +551,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 +615,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 +658,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 { @@ -616,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". @@ -633,12 +862,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 { @@ -767,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; } @@ -813,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; } @@ -844,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 4ab722c122..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: 11 +// version: 15 declare module 'vscode' { @@ -110,6 +110,31 @@ 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; + + /** + * 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 { @@ -127,6 +152,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. * @@ -162,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); + 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 { @@ -244,10 +283,10 @@ 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 */ - chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; terminalCommand?: string; @@ -255,6 +294,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 { @@ -263,10 +312,12 @@ declare module 'vscode' { */ input: T; chatRequestId?: string; - /** @deprecated Use {@link chatSessionResource} instead */ - chatSessionId?: string; chatSessionResource?: Uri; chatInteractionId?: string; + /** + * If set, tells the tool that it should include confirmation messages. + */ + forceConfirmationReason?: string; } export interface PreparedToolInvocation { @@ -305,6 +356,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 +403,37 @@ 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; + + /** + * 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 84cd547599..06d0649b18 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -23,13 +23,20 @@ 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 { /** * 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. @@ -41,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: () => 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 { /** @@ -72,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; @@ -88,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; @@ -97,12 +170,39 @@ declare module 'vscode' { * * This is also called on first load to get the initial set of items. */ - refreshHandler: () => 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; } /** @@ -116,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; @@ -131,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 @@ -199,56 +311,51 @@ 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 }; } + /** + * @deprecated Use `ChatSessionChangedFile2` instead + */ export class ChatSessionChangedFile { /** * URI of the file. @@ -303,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 * @@ -338,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; } /** @@ -370,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} @@ -389,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. @@ -402,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 { @@ -426,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 { @@ -438,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 { @@ -505,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. @@ -520,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[]; } @@ -554,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/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 88e3784b06..1b5cacb621 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -825,6 +825,117 @@ export function registerCommands( ), ); + context.subscriptions.push( + 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.')); + } + + let pullRequestModel: PullRequestModel; + let repository: Repository | undefined; + + 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) { + 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) { + 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'); + + // 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; + + // 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 + } + + const worktreePath = worktreeUri.fsPath; + 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.')); + } + + // 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, + commitish: trackedBranchName, + branch: branchName + }); + } + ); + + // 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)); + } + }), + ); + 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.')); 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({